rust.rs

   1use anyhow::{Context as _, Result, anyhow};
   2use async_compression::futures::bufread::GzipDecoder;
   3use async_trait::async_trait;
   4use collections::HashMap;
   5use futures::{StreamExt, io::BufReader};
   6use gpui::{App, AppContext, AsyncApp, SharedString, Task};
   7use http_client::github::AssetKind;
   8use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
   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 smol::fs::{self};
  17use std::fmt::Display;
  18use std::{
  19    any::Any,
  20    borrow::Cow,
  21    path::{Path, PathBuf},
  22    sync::{Arc, LazyLock},
  23};
  24use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
  25use util::merge_json_value_into;
  26use util::{ResultExt, fs::remove_matching, maybe};
  27
  28use crate::language_settings::language_settings;
  29
  30pub struct RustLspAdapter;
  31
  32#[cfg(target_os = "macos")]
  33impl RustLspAdapter {
  34    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  35    const ARCH_SERVER_NAME: &str = "apple-darwin";
  36}
  37
  38#[cfg(target_os = "linux")]
  39impl RustLspAdapter {
  40    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  41    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
  42}
  43
  44#[cfg(target_os = "freebsd")]
  45impl RustLspAdapter {
  46    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  47    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
  48}
  49
  50#[cfg(target_os = "windows")]
  51impl RustLspAdapter {
  52    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
  53    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
  54}
  55
  56const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
  57
  58impl RustLspAdapter {
  59    fn build_asset_name() -> String {
  60        let extension = match Self::GITHUB_ASSET_KIND {
  61            AssetKind::TarGz => "tar.gz",
  62            AssetKind::Gz => "gz",
  63            AssetKind::Zip => "zip",
  64        };
  65
  66        format!(
  67            "{}-{}-{}.{}",
  68            SERVER_NAME,
  69            std::env::consts::ARCH,
  70            Self::ARCH_SERVER_NAME,
  71            extension
  72        )
  73    }
  74}
  75
  76pub(crate) struct CargoManifestProvider;
  77
  78impl ManifestProvider for CargoManifestProvider {
  79    fn name(&self) -> ManifestName {
  80        SharedString::new_static("Cargo.toml").into()
  81    }
  82
  83    fn search(
  84        &self,
  85        ManifestQuery {
  86            path,
  87            depth,
  88            delegate,
  89        }: ManifestQuery,
  90    ) -> Option<Arc<Path>> {
  91        let mut outermost_cargo_toml = None;
  92        for path in path.ancestors().take(depth) {
  93            let p = path.join("Cargo.toml");
  94            if delegate.exists(&p, Some(false)) {
  95                outermost_cargo_toml = Some(Arc::from(path));
  96            }
  97        }
  98
  99        outermost_cargo_toml
 100    }
 101}
 102
 103#[async_trait(?Send)]
 104impl LspAdapter for RustLspAdapter {
 105    fn name(&self) -> LanguageServerName {
 106        SERVER_NAME.clone()
 107    }
 108
 109    fn manifest_name(&self) -> Option<ManifestName> {
 110        Some(SharedString::new_static("Cargo.toml").into())
 111    }
 112
 113    async fn check_if_user_installed(
 114        &self,
 115        delegate: &dyn LspAdapterDelegate,
 116        _: Arc<dyn LanguageToolchainStore>,
 117        _: &AsyncApp,
 118    ) -> Option<LanguageServerBinary> {
 119        let path = delegate.which("rust-analyzer".as_ref()).await?;
 120        let env = delegate.shell_env().await;
 121
 122        // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
 123        // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
 124        log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
 125        let result = delegate
 126            .try_exec(LanguageServerBinary {
 127                path: path.clone(),
 128                arguments: vec!["--help".into()],
 129                env: Some(env.clone()),
 130            })
 131            .await;
 132        if let Err(err) = result {
 133            log::debug!(
 134                "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
 135                path,
 136                err
 137            );
 138            return None;
 139        }
 140
 141        Some(LanguageServerBinary {
 142            path,
 143            env: Some(env),
 144            arguments: vec![],
 145        })
 146    }
 147
 148    async fn fetch_latest_server_version(
 149        &self,
 150        delegate: &dyn LspAdapterDelegate,
 151    ) -> Result<Box<dyn 'static + Send + Any>> {
 152        let release = latest_github_release(
 153            "rust-lang/rust-analyzer",
 154            true,
 155            false,
 156            delegate.http_client(),
 157        )
 158        .await?;
 159        let asset_name = Self::build_asset_name();
 160
 161        let asset = release
 162            .assets
 163            .iter()
 164            .find(|asset| asset.name == asset_name)
 165            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 166        Ok(Box::new(GitHubLspBinaryVersion {
 167            name: release.tag_name,
 168            url: asset.browser_download_url.clone(),
 169        }))
 170    }
 171
 172    async fn fetch_server_binary(
 173        &self,
 174        version: Box<dyn 'static + Send + Any>,
 175        container_dir: PathBuf,
 176        delegate: &dyn LspAdapterDelegate,
 177    ) -> Result<LanguageServerBinary> {
 178        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 179        let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 180        let server_path = match Self::GITHUB_ASSET_KIND {
 181            AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
 182            AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
 183        };
 184
 185        if fs::metadata(&server_path).await.is_err() {
 186            remove_matching(&container_dir, |entry| entry != destination_path).await;
 187
 188            let mut response = delegate
 189                .http_client()
 190                .get(&version.url, Default::default(), true)
 191                .await
 192                .with_context(|| format!("downloading release from {}", version.url))?;
 193            match Self::GITHUB_ASSET_KIND {
 194                AssetKind::TarGz => {
 195                    let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
 196                    let archive = async_tar::Archive::new(decompressed_bytes);
 197                    archive.unpack(&destination_path).await.with_context(|| {
 198                        format!("extracting {} to {:?}", version.url, destination_path)
 199                    })?;
 200                }
 201                AssetKind::Gz => {
 202                    let mut decompressed_bytes =
 203                        GzipDecoder::new(BufReader::new(response.body_mut()));
 204                    let mut file =
 205                        fs::File::create(&destination_path).await.with_context(|| {
 206                            format!(
 207                                "creating a file {:?} for a download from {}",
 208                                destination_path, version.url,
 209                            )
 210                        })?;
 211                    futures::io::copy(&mut decompressed_bytes, &mut file)
 212                        .await
 213                        .with_context(|| {
 214                            format!("extracting {} to {:?}", version.url, destination_path)
 215                        })?;
 216                }
 217                AssetKind::Zip => {
 218                    node_runtime::extract_zip(
 219                        &destination_path,
 220                        BufReader::new(response.body_mut()),
 221                    )
 222                    .await
 223                    .with_context(|| {
 224                        format!("unzipping {} to {:?}", version.url, destination_path)
 225                    })?;
 226                }
 227            };
 228
 229            // todo("windows")
 230            #[cfg(not(windows))]
 231            {
 232                fs::set_permissions(
 233                    &server_path,
 234                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
 235                )
 236                .await?;
 237            }
 238        }
 239
 240        Ok(LanguageServerBinary {
 241            path: server_path,
 242            env: None,
 243            arguments: Default::default(),
 244        })
 245    }
 246
 247    async fn cached_server_binary(
 248        &self,
 249        container_dir: PathBuf,
 250        _: &dyn LspAdapterDelegate,
 251    ) -> Option<LanguageServerBinary> {
 252        get_cached_server_binary(container_dir).await
 253    }
 254
 255    fn disk_based_diagnostic_sources(&self) -> Vec<String> {
 256        vec![CARGO_DIAGNOSTICS_SOURCE_NAME.to_owned()]
 257    }
 258
 259    fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
 260        Some("rust-analyzer/flycheck".into())
 261    }
 262
 263    fn process_diagnostics(
 264        &self,
 265        params: &mut lsp::PublishDiagnosticsParams,
 266        _: LanguageServerId,
 267        _: Option<&'_ Buffer>,
 268    ) {
 269        static REGEX: LazyLock<Regex> =
 270            LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
 271
 272        for diagnostic in &mut params.diagnostics {
 273            for message in diagnostic
 274                .related_information
 275                .iter_mut()
 276                .flatten()
 277                .map(|info| &mut info.message)
 278                .chain([&mut diagnostic.message])
 279            {
 280                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
 281                    *message = sanitized;
 282                }
 283            }
 284        }
 285    }
 286
 287    fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
 288        static REGEX: LazyLock<Regex> =
 289            LazyLock::new(|| Regex::new(r"(?m)\n *").expect("Failed to create REGEX"));
 290        Some(REGEX.replace_all(message, "\n\n").to_string())
 291    }
 292
 293    async fn label_for_completion(
 294        &self,
 295        completion: &lsp::CompletionItem,
 296        language: &Arc<Language>,
 297    ) -> Option<CodeLabel> {
 298        let detail = completion
 299            .label_details
 300            .as_ref()
 301            .and_then(|detail| detail.detail.as_ref())
 302            .or(completion.detail.as_ref())
 303            .map(|detail| detail.trim());
 304        let function_signature = completion
 305            .label_details
 306            .as_ref()
 307            .and_then(|detail| detail.description.as_deref())
 308            .or(completion.detail.as_deref());
 309        match (detail, completion.kind) {
 310            (Some(detail), Some(lsp::CompletionItemKind::FIELD)) => {
 311                let name = &completion.label;
 312                let text = format!("{name}: {detail}");
 313                let prefix = "struct S { ";
 314                let source = Rope::from(format!("{prefix}{text} }}"));
 315                let runs =
 316                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
 317                return Some(CodeLabel {
 318                    text,
 319                    runs,
 320                    filter_range: 0..name.len(),
 321                });
 322            }
 323            (
 324                Some(detail),
 325                Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
 326            ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
 327                let name = &completion.label;
 328                let text = format!(
 329                    "{}: {}",
 330                    name,
 331                    completion.detail.as_deref().unwrap_or(detail)
 332                );
 333                let prefix = "let ";
 334                let source = Rope::from(format!("{prefix}{text} = ();"));
 335                let runs =
 336                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
 337                return Some(CodeLabel {
 338                    text,
 339                    runs,
 340                    filter_range: 0..name.len(),
 341                });
 342            }
 343            (
 344                Some(detail),
 345                Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
 346            ) => {
 347                static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
 348                const FUNCTION_PREFIXES: [&str; 6] = [
 349                    "async fn",
 350                    "async unsafe fn",
 351                    "const fn",
 352                    "const unsafe fn",
 353                    "unsafe fn",
 354                    "fn",
 355                ];
 356                // Is it function `async`?
 357                let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| {
 358                    function_signature.as_ref().and_then(|signature| {
 359                        signature
 360                            .strip_prefix(*prefix)
 361                            .map(|suffix| (*prefix, suffix))
 362                    })
 363                });
 364                // fn keyword should be followed by opening parenthesis.
 365                if let Some((prefix, suffix)) = fn_keyword {
 366                    let mut text = REGEX.replace(&completion.label, suffix).to_string();
 367                    let source = Rope::from(format!("{prefix} {text} {{}}"));
 368                    let run_start = prefix.len() + 1;
 369                    let runs = language.highlight_text(&source, run_start..run_start + text.len());
 370                    if detail.starts_with("(") {
 371                        text.push(' ');
 372                        text.push_str(&detail);
 373                    }
 374
 375                    return Some(CodeLabel {
 376                        filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
 377                        text,
 378                        runs,
 379                    });
 380                } else if completion
 381                    .detail
 382                    .as_ref()
 383                    .map_or(false, |detail| detail.starts_with("macro_rules! "))
 384                {
 385                    let source = Rope::from(completion.label.as_str());
 386                    let runs = language.highlight_text(&source, 0..completion.label.len());
 387
 388                    return Some(CodeLabel {
 389                        filter_range: 0..completion.label.len(),
 390                        text: completion.label.clone(),
 391                        runs,
 392                    });
 393                }
 394            }
 395            (_, Some(kind)) => {
 396                let highlight_name = match kind {
 397                    lsp::CompletionItemKind::STRUCT
 398                    | lsp::CompletionItemKind::INTERFACE
 399                    | lsp::CompletionItemKind::ENUM => Some("type"),
 400                    lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
 401                    lsp::CompletionItemKind::KEYWORD => Some("keyword"),
 402                    lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
 403                        Some("constant")
 404                    }
 405                    _ => None,
 406                };
 407
 408                let mut label = completion.label.clone();
 409                if let Some(detail) = detail.filter(|detail| detail.starts_with("(")) {
 410                    label.push(' ');
 411                    label.push_str(detail);
 412                }
 413                let mut label = CodeLabel::plain(label, None);
 414                if let Some(highlight_name) = highlight_name {
 415                    let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
 416                    label.runs.push((
 417                        0..label.text.rfind('(').unwrap_or(completion.label.len()),
 418                        highlight_id,
 419                    ));
 420                }
 421
 422                return Some(label);
 423            }
 424            _ => {}
 425        }
 426        None
 427    }
 428
 429    async fn label_for_symbol(
 430        &self,
 431        name: &str,
 432        kind: lsp::SymbolKind,
 433        language: &Arc<Language>,
 434    ) -> Option<CodeLabel> {
 435        let (text, filter_range, display_range) = match kind {
 436            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
 437                let text = format!("fn {} () {{}}", name);
 438                let filter_range = 3..3 + name.len();
 439                let display_range = 0..filter_range.end;
 440                (text, filter_range, display_range)
 441            }
 442            lsp::SymbolKind::STRUCT => {
 443                let text = format!("struct {} {{}}", name);
 444                let filter_range = 7..7 + name.len();
 445                let display_range = 0..filter_range.end;
 446                (text, filter_range, display_range)
 447            }
 448            lsp::SymbolKind::ENUM => {
 449                let text = format!("enum {} {{}}", name);
 450                let filter_range = 5..5 + name.len();
 451                let display_range = 0..filter_range.end;
 452                (text, filter_range, display_range)
 453            }
 454            lsp::SymbolKind::INTERFACE => {
 455                let text = format!("trait {} {{}}", name);
 456                let filter_range = 6..6 + name.len();
 457                let display_range = 0..filter_range.end;
 458                (text, filter_range, display_range)
 459            }
 460            lsp::SymbolKind::CONSTANT => {
 461                let text = format!("const {}: () = ();", name);
 462                let filter_range = 6..6 + name.len();
 463                let display_range = 0..filter_range.end;
 464                (text, filter_range, display_range)
 465            }
 466            lsp::SymbolKind::MODULE => {
 467                let text = format!("mod {} {{}}", name);
 468                let filter_range = 4..4 + name.len();
 469                let display_range = 0..filter_range.end;
 470                (text, filter_range, display_range)
 471            }
 472            lsp::SymbolKind::TYPE_PARAMETER => {
 473                let text = format!("type {} {{}}", name);
 474                let filter_range = 5..5 + name.len();
 475                let display_range = 0..filter_range.end;
 476                (text, filter_range, display_range)
 477            }
 478            _ => return None,
 479        };
 480
 481        Some(CodeLabel {
 482            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
 483            text: text[display_range].to_string(),
 484            filter_range,
 485        })
 486    }
 487
 488    fn prepare_initialize_params(
 489        &self,
 490        mut original: InitializeParams,
 491        cx: &App,
 492    ) -> Result<InitializeParams> {
 493        let enable_lsp_tasks = ProjectSettings::get_global(cx)
 494            .lsp
 495            .get(&SERVER_NAME)
 496            .map_or(false, |s| s.enable_lsp_tasks);
 497        if enable_lsp_tasks {
 498            let experimental = json!({
 499                "runnables": {
 500                    "kinds": [ "cargo", "shell" ],
 501                },
 502            });
 503            if let Some(original_experimental) = &mut original.capabilities.experimental {
 504                merge_json_value_into(experimental, original_experimental);
 505            } else {
 506                original.capabilities.experimental = Some(experimental);
 507            }
 508        }
 509
 510        let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx)
 511            .diagnostics
 512            .fetch_cargo_diagnostics();
 513        if cargo_diagnostics_fetched_separately {
 514            let disable_check_on_save = json!({
 515                "checkOnSave": false,
 516            });
 517            if let Some(initialization_options) = &mut original.initialization_options {
 518                merge_json_value_into(disable_check_on_save, initialization_options);
 519            } else {
 520                original.initialization_options = Some(disable_check_on_save);
 521            }
 522        }
 523
 524        Ok(original)
 525    }
 526}
 527
 528pub(crate) struct RustContextProvider;
 529
 530const RUST_PACKAGE_TASK_VARIABLE: VariableName =
 531    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
 532
 533/// The bin name corresponding to the current file in Cargo.toml
 534const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
 535    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
 536
 537/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
 538const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
 539    VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
 540
 541/// The flag to list required features for executing a bin, if any
 542const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
 543    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
 544
 545/// The list of required features for executing a bin, if any
 546const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
 547    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
 548
 549const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
 550    VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
 551
 552const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
 553    VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
 554
 555const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
 556    VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
 557
 558impl ContextProvider for RustContextProvider {
 559    fn build_context(
 560        &self,
 561        task_variables: &TaskVariables,
 562        location: &Location,
 563        project_env: Option<HashMap<String, String>>,
 564        _: Arc<dyn LanguageToolchainStore>,
 565        cx: &mut gpui::App,
 566    ) -> Task<Result<TaskVariables>> {
 567        let local_abs_path = location
 568            .buffer
 569            .read(cx)
 570            .file()
 571            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 572
 573        let mut variables = TaskVariables::default();
 574
 575        if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
 576        {
 577            let fragment = test_fragment(&variables, &path, stem);
 578            variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
 579        };
 580        if let Some(test_name) =
 581            task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
 582        {
 583            variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
 584        }
 585        if let Some(doc_test_name) =
 586            task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
 587        {
 588            variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
 589        }
 590        cx.background_spawn(async move {
 591            if let Some(path) = local_abs_path
 592                .as_deref()
 593                .and_then(|local_abs_path| local_abs_path.parent())
 594            {
 595                if let Some(package_name) =
 596                    human_readable_package_name(path, project_env.as_ref()).await
 597                {
 598                    variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
 599                }
 600            }
 601            if let Some(path) = local_abs_path.as_ref() {
 602                if let Some(target) = target_info_from_abs_path(&path, project_env.as_ref()).await {
 603                    variables.extend(TaskVariables::from_iter([
 604                        (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
 605                        (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
 606                        (
 607                            RUST_BIN_KIND_TASK_VARIABLE.clone(),
 608                            target.target_kind.to_string(),
 609                        ),
 610                    ]));
 611                    if target.required_features.is_empty() {
 612                        variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
 613                        variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
 614                    } else {
 615                        variables.insert(
 616                            RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
 617                            "--features".to_string(),
 618                        );
 619                        variables.insert(
 620                            RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
 621                            target.required_features.join(","),
 622                        );
 623                    }
 624                }
 625            }
 626            Ok(variables)
 627        })
 628    }
 629
 630    fn associated_tasks(
 631        &self,
 632        file: Option<Arc<dyn language::File>>,
 633        cx: &App,
 634    ) -> Option<TaskTemplates> {
 635        const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
 636        const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
 637
 638        let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
 639        let package_to_run = language_sets
 640            .tasks
 641            .variables
 642            .get(DEFAULT_RUN_NAME_STR)
 643            .cloned();
 644        let custom_target_dir = language_sets
 645            .tasks
 646            .variables
 647            .get(CUSTOM_TARGET_DIR)
 648            .cloned();
 649        let run_task_args = if let Some(package_to_run) = package_to_run.clone() {
 650            vec!["run".into(), "-p".into(), package_to_run]
 651        } else {
 652            vec!["run".into()]
 653        };
 654        let mut task_templates = vec![
 655            TaskTemplate {
 656                label: format!(
 657                    "Check (package: {})",
 658                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 659                ),
 660                command: "cargo".into(),
 661                args: vec![
 662                    "check".into(),
 663                    "-p".into(),
 664                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 665                ],
 666                cwd: Some("$ZED_DIRNAME".to_owned()),
 667                ..TaskTemplate::default()
 668            },
 669            TaskTemplate {
 670                label: "Check all targets (workspace)".into(),
 671                command: "cargo".into(),
 672                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
 673                cwd: Some("$ZED_DIRNAME".to_owned()),
 674                ..TaskTemplate::default()
 675            },
 676            TaskTemplate {
 677                label: format!(
 678                    "Test '{}' (package: {})",
 679                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 680                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 681                ),
 682                command: "cargo".into(),
 683                args: vec![
 684                    "test".into(),
 685                    "-p".into(),
 686                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 687                    "--".into(),
 688                    "--nocapture".into(),
 689                    "--include-ignored".into(),
 690                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 691                ],
 692                tags: vec!["rust-test".to_owned()],
 693                cwd: Some("$ZED_DIRNAME".to_owned()),
 694                ..TaskTemplate::default()
 695            },
 696            TaskTemplate {
 697                label: format!(
 698                    "Doc test '{}' (package: {})",
 699                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 700                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 701                ),
 702                command: "cargo".into(),
 703                args: vec![
 704                    "test".into(),
 705                    "--doc".into(),
 706                    "-p".into(),
 707                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 708                    "--".into(),
 709                    "--nocapture".into(),
 710                    "--include-ignored".into(),
 711                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 712                ],
 713                tags: vec!["rust-doc-test".to_owned()],
 714                cwd: Some("$ZED_DIRNAME".to_owned()),
 715                ..TaskTemplate::default()
 716            },
 717            TaskTemplate {
 718                label: format!(
 719                    "Test mod '{}' (package: {})",
 720                    VariableName::Stem.template_value(),
 721                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 722                ),
 723                command: "cargo".into(),
 724                args: vec![
 725                    "test".into(),
 726                    "-p".into(),
 727                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 728                    "--".into(),
 729                    RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
 730                ],
 731                tags: vec!["rust-mod-test".to_owned()],
 732                cwd: Some("$ZED_DIRNAME".to_owned()),
 733                ..TaskTemplate::default()
 734            },
 735            TaskTemplate {
 736                label: format!(
 737                    "Run {} {} (package: {})",
 738                    RUST_BIN_KIND_TASK_VARIABLE.template_value(),
 739                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 740                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 741                ),
 742                command: "cargo".into(),
 743                args: vec![
 744                    "run".into(),
 745                    "-p".into(),
 746                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 747                    format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
 748                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 749                    RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
 750                    RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
 751                ],
 752                cwd: Some("$ZED_DIRNAME".to_owned()),
 753                tags: vec!["rust-main".to_owned()],
 754                ..TaskTemplate::default()
 755            },
 756            TaskTemplate {
 757                label: format!(
 758                    "Test (package: {})",
 759                    RUST_PACKAGE_TASK_VARIABLE.template_value()
 760                ),
 761                command: "cargo".into(),
 762                args: vec![
 763                    "test".into(),
 764                    "-p".into(),
 765                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 766                ],
 767                cwd: Some("$ZED_DIRNAME".to_owned()),
 768                ..TaskTemplate::default()
 769            },
 770            TaskTemplate {
 771                label: "Run".into(),
 772                command: "cargo".into(),
 773                args: run_task_args,
 774                cwd: Some("$ZED_DIRNAME".to_owned()),
 775                ..TaskTemplate::default()
 776            },
 777            TaskTemplate {
 778                label: "Clean".into(),
 779                command: "cargo".into(),
 780                args: vec!["clean".into()],
 781                cwd: Some("$ZED_DIRNAME".to_owned()),
 782                ..TaskTemplate::default()
 783            },
 784        ];
 785
 786        if let Some(custom_target_dir) = custom_target_dir {
 787            task_templates = task_templates
 788                .into_iter()
 789                .map(|mut task_template| {
 790                    let mut args = task_template.args.split_off(1);
 791                    task_template.args.append(&mut vec![
 792                        "--target-dir".to_string(),
 793                        custom_target_dir.clone(),
 794                    ]);
 795                    task_template.args.append(&mut args);
 796
 797                    task_template
 798                })
 799                .collect();
 800        }
 801
 802        Some(TaskTemplates(task_templates))
 803    }
 804
 805    fn lsp_task_source(&self) -> Option<LanguageServerName> {
 806        Some(SERVER_NAME)
 807    }
 808}
 809
 810/// Part of the data structure of Cargo metadata
 811#[derive(serde::Deserialize)]
 812struct CargoMetadata {
 813    packages: Vec<CargoPackage>,
 814}
 815
 816#[derive(serde::Deserialize)]
 817struct CargoPackage {
 818    id: String,
 819    targets: Vec<CargoTarget>,
 820}
 821
 822#[derive(serde::Deserialize)]
 823struct CargoTarget {
 824    name: String,
 825    kind: Vec<String>,
 826    src_path: String,
 827    #[serde(rename = "required-features", default)]
 828    required_features: Vec<String>,
 829}
 830
 831#[derive(Debug, PartialEq)]
 832enum TargetKind {
 833    Bin,
 834    Example,
 835}
 836
 837impl Display for TargetKind {
 838    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 839        match self {
 840            TargetKind::Bin => write!(f, "bin"),
 841            TargetKind::Example => write!(f, "example"),
 842        }
 843    }
 844}
 845
 846impl TryFrom<&str> for TargetKind {
 847    type Error = ();
 848    fn try_from(value: &str) -> Result<Self, ()> {
 849        match value {
 850            "bin" => Ok(Self::Bin),
 851            "example" => Ok(Self::Example),
 852            _ => Err(()),
 853        }
 854    }
 855}
 856/// Which package and binary target are we in?
 857#[derive(Debug, PartialEq)]
 858struct TargetInfo {
 859    package_name: String,
 860    target_name: String,
 861    target_kind: TargetKind,
 862    required_features: Vec<String>,
 863}
 864
 865async fn target_info_from_abs_path(
 866    abs_path: &Path,
 867    project_env: Option<&HashMap<String, String>>,
 868) -> Option<TargetInfo> {
 869    let mut command = util::command::new_smol_command("cargo");
 870    if let Some(envs) = project_env {
 871        command.envs(envs);
 872    }
 873    let output = command
 874        .current_dir(abs_path.parent()?)
 875        .arg("metadata")
 876        .arg("--no-deps")
 877        .arg("--format-version")
 878        .arg("1")
 879        .output()
 880        .await
 881        .log_err()?
 882        .stdout;
 883
 884    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
 885
 886    target_info_from_metadata(metadata, abs_path)
 887}
 888
 889fn target_info_from_metadata(metadata: CargoMetadata, abs_path: &Path) -> Option<TargetInfo> {
 890    for package in metadata.packages {
 891        for target in package.targets {
 892            let Some(bin_kind) = target
 893                .kind
 894                .iter()
 895                .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
 896            else {
 897                continue;
 898            };
 899            let target_path = PathBuf::from(target.src_path);
 900            if target_path == abs_path {
 901                return package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
 902                    package_name: package_name.to_owned(),
 903                    target_name: target.name,
 904                    required_features: target.required_features,
 905                    target_kind: bin_kind,
 906                });
 907            }
 908        }
 909    }
 910
 911    None
 912}
 913
 914async fn human_readable_package_name(
 915    package_directory: &Path,
 916    project_env: Option<&HashMap<String, String>>,
 917) -> Option<String> {
 918    let mut command = util::command::new_smol_command("cargo");
 919    if let Some(envs) = project_env {
 920        command.envs(envs);
 921    }
 922    let pkgid = String::from_utf8(
 923        command
 924            .current_dir(package_directory)
 925            .arg("pkgid")
 926            .output()
 927            .await
 928            .log_err()?
 929            .stdout,
 930    )
 931    .ok()?;
 932    Some(package_name_from_pkgid(&pkgid)?.to_owned())
 933}
 934
 935// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
 936// Output example in the root of Zed project:
 937// ```sh
 938// ❯ cargo pkgid zed
 939// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
 940// ```
 941// Another variant, if a project has a custom package name or hyphen in the name:
 942// ```
 943// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
 944// ```
 945//
 946// Extracts the package name from the output according to the spec:
 947// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
 948fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
 949    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
 950        match input.rsplit_once(suffix_start) {
 951            Some((without_suffix, _)) => without_suffix,
 952            None => input,
 953        }
 954    }
 955
 956    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
 957    let package_name = match version_suffix.rsplit_once('@') {
 958        Some((custom_package_name, _version)) => custom_package_name,
 959        None => {
 960            let host_and_path = split_off_suffix(version_prefix, '?');
 961            let (_, package_name) = host_and_path.rsplit_once('/')?;
 962            package_name
 963        }
 964    };
 965    Some(package_name)
 966}
 967
 968async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
 969    maybe!(async {
 970        let mut last = None;
 971        let mut entries = fs::read_dir(&container_dir).await?;
 972        while let Some(entry) = entries.next().await {
 973            last = Some(entry?.path());
 974        }
 975
 976        anyhow::Ok(LanguageServerBinary {
 977            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
 978            env: None,
 979            arguments: Default::default(),
 980        })
 981    })
 982    .await
 983    .log_err()
 984}
 985
 986fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
 987    let fragment = if stem == "lib" {
 988        // This isn't quite right---it runs the tests for the entire library, rather than
 989        // just for the top-level `mod tests`. But we don't really have the means here to
 990        // filter out just that module.
 991        Some("--lib".to_owned())
 992    } else if stem == "mod" {
 993        maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().to_string()) })
 994    } else if stem == "main" {
 995        if let (Some(bin_name), Some(bin_kind)) = (
 996            variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
 997            variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
 998        ) {
 999            Some(format!("--{bin_kind}={bin_name}"))
1000        } else {
1001            None
1002        }
1003    } else {
1004        Some(stem.to_owned())
1005    };
1006    fragment.unwrap_or_else(|| "--".to_owned())
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011    use std::num::NonZeroU32;
1012
1013    use super::*;
1014    use crate::language;
1015    use gpui::{BorrowAppContext, Hsla, TestAppContext};
1016    use language::language_settings::AllLanguageSettings;
1017    use lsp::CompletionItemLabelDetails;
1018    use settings::SettingsStore;
1019    use theme::SyntaxTheme;
1020    use util::path;
1021
1022    #[gpui::test]
1023    async fn test_process_rust_diagnostics() {
1024        let mut params = lsp::PublishDiagnosticsParams {
1025            uri: lsp::Url::from_file_path(path!("/a")).unwrap(),
1026            version: None,
1027            diagnostics: vec![
1028                // no newlines
1029                lsp::Diagnostic {
1030                    message: "use of moved value `a`".to_string(),
1031                    ..Default::default()
1032                },
1033                // newline at the end of a code span
1034                lsp::Diagnostic {
1035                    message: "consider importing this struct: `use b::c;\n`".to_string(),
1036                    ..Default::default()
1037                },
1038                // code span starting right after a newline
1039                lsp::Diagnostic {
1040                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1041                        .to_string(),
1042                    ..Default::default()
1043                },
1044            ],
1045        };
1046        RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1047
1048        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1049
1050        // remove trailing newline from code span
1051        assert_eq!(
1052            params.diagnostics[1].message,
1053            "consider importing this struct: `use b::c;`"
1054        );
1055
1056        // do not remove newline before the start of code span
1057        assert_eq!(
1058            params.diagnostics[2].message,
1059            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1060        );
1061    }
1062
1063    #[gpui::test]
1064    async fn test_rust_label_for_completion() {
1065        let adapter = Arc::new(RustLspAdapter);
1066        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1067        let grammar = language.grammar().unwrap();
1068        let theme = SyntaxTheme::new_test([
1069            ("type", Hsla::default()),
1070            ("keyword", Hsla::default()),
1071            ("function", Hsla::default()),
1072            ("property", Hsla::default()),
1073        ]);
1074
1075        language.set_theme(&theme);
1076
1077        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1078        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1079        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1080        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1081
1082        assert_eq!(
1083            adapter
1084                .label_for_completion(
1085                    &lsp::CompletionItem {
1086                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1087                        label: "hello(…)".to_string(),
1088                        label_details: Some(CompletionItemLabelDetails {
1089                            detail: Some("(use crate::foo)".into()),
1090                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1091                        }),
1092                        ..Default::default()
1093                    },
1094                    &language
1095                )
1096                .await,
1097            Some(CodeLabel {
1098                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1099                filter_range: 0..5,
1100                runs: vec![
1101                    (0..5, highlight_function),
1102                    (7..10, highlight_keyword),
1103                    (11..17, highlight_type),
1104                    (18..19, highlight_type),
1105                    (25..28, highlight_type),
1106                    (29..30, highlight_type),
1107                ],
1108            })
1109        );
1110        assert_eq!(
1111            adapter
1112                .label_for_completion(
1113                    &lsp::CompletionItem {
1114                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1115                        label: "hello(…)".to_string(),
1116                        label_details: Some(CompletionItemLabelDetails {
1117                            detail: Some(" (use crate::foo)".into()),
1118                            description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1119                        }),
1120                        ..Default::default()
1121                    },
1122                    &language
1123                )
1124                .await,
1125            Some(CodeLabel {
1126                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1127                filter_range: 0..5,
1128                runs: vec![
1129                    (0..5, highlight_function),
1130                    (7..10, highlight_keyword),
1131                    (11..17, highlight_type),
1132                    (18..19, highlight_type),
1133                    (25..28, highlight_type),
1134                    (29..30, highlight_type),
1135                ],
1136            })
1137        );
1138        assert_eq!(
1139            adapter
1140                .label_for_completion(
1141                    &lsp::CompletionItem {
1142                        kind: Some(lsp::CompletionItemKind::FIELD),
1143                        label: "len".to_string(),
1144                        detail: Some("usize".to_string()),
1145                        ..Default::default()
1146                    },
1147                    &language
1148                )
1149                .await,
1150            Some(CodeLabel {
1151                text: "len: usize".to_string(),
1152                filter_range: 0..3,
1153                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
1154            })
1155        );
1156
1157        assert_eq!(
1158            adapter
1159                .label_for_completion(
1160                    &lsp::CompletionItem {
1161                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1162                        label: "hello(…)".to_string(),
1163                        label_details: Some(CompletionItemLabelDetails {
1164                            detail: Some(" (use crate::foo)".to_string()),
1165                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1166                        }),
1167
1168                        ..Default::default()
1169                    },
1170                    &language
1171                )
1172                .await,
1173            Some(CodeLabel {
1174                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1175                filter_range: 0..5,
1176                runs: vec![
1177                    (0..5, highlight_function),
1178                    (7..10, highlight_keyword),
1179                    (11..17, highlight_type),
1180                    (18..19, highlight_type),
1181                    (25..28, highlight_type),
1182                    (29..30, highlight_type),
1183                ],
1184            })
1185        );
1186    }
1187
1188    #[gpui::test]
1189    async fn test_rust_label_for_symbol() {
1190        let adapter = Arc::new(RustLspAdapter);
1191        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1192        let grammar = language.grammar().unwrap();
1193        let theme = SyntaxTheme::new_test([
1194            ("type", Hsla::default()),
1195            ("keyword", Hsla::default()),
1196            ("function", Hsla::default()),
1197            ("property", Hsla::default()),
1198        ]);
1199
1200        language.set_theme(&theme);
1201
1202        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1203        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1204        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1205
1206        assert_eq!(
1207            adapter
1208                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1209                .await,
1210            Some(CodeLabel {
1211                text: "fn hello".to_string(),
1212                filter_range: 3..8,
1213                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1214            })
1215        );
1216
1217        assert_eq!(
1218            adapter
1219                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1220                .await,
1221            Some(CodeLabel {
1222                text: "type World".to_string(),
1223                filter_range: 5..10,
1224                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1225            })
1226        );
1227    }
1228
1229    #[gpui::test]
1230    async fn test_rust_autoindent(cx: &mut TestAppContext) {
1231        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1232        cx.update(|cx| {
1233            let test_settings = SettingsStore::test(cx);
1234            cx.set_global(test_settings);
1235            language::init(cx);
1236            cx.update_global::<SettingsStore, _>(|store, cx| {
1237                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
1238                    s.defaults.tab_size = NonZeroU32::new(2);
1239                });
1240            });
1241        });
1242
1243        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1244
1245        cx.new(|cx| {
1246            let mut buffer = Buffer::local("", cx).with_language(language, cx);
1247
1248            // indent between braces
1249            buffer.set_text("fn a() {}", cx);
1250            let ix = buffer.len() - 1;
1251            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1252            assert_eq!(buffer.text(), "fn a() {\n  \n}");
1253
1254            // indent between braces, even after empty lines
1255            buffer.set_text("fn a() {\n\n\n}", cx);
1256            let ix = buffer.len() - 2;
1257            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1258            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1259
1260            // indent a line that continues a field expression
1261            buffer.set_text("fn a() {\n  \n}", cx);
1262            let ix = buffer.len() - 2;
1263            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1264            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1265
1266            // indent further lines that continue the field expression, even after empty lines
1267            let ix = buffer.len() - 2;
1268            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1269            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1270
1271            // dedent the line after the field expression
1272            let ix = buffer.len() - 2;
1273            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1274            assert_eq!(
1275                buffer.text(),
1276                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1277            );
1278
1279            // indent inside a struct within a call
1280            buffer.set_text("const a: B = c(D {});", cx);
1281            let ix = buffer.len() - 3;
1282            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1283            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1284
1285            // indent further inside a nested call
1286            let ix = buffer.len() - 4;
1287            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1288            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1289
1290            // keep that indent after an empty line
1291            let ix = buffer.len() - 8;
1292            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1293            assert_eq!(
1294                buffer.text(),
1295                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1296            );
1297
1298            buffer
1299        });
1300    }
1301
1302    #[test]
1303    fn test_package_name_from_pkgid() {
1304        for (input, expected) in [
1305            (
1306                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1307                "zed",
1308            ),
1309            (
1310                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1311                "my-custom-package",
1312            ),
1313        ] {
1314            assert_eq!(package_name_from_pkgid(input), Some(expected));
1315        }
1316    }
1317
1318    #[test]
1319    fn test_target_info_from_metadata() {
1320        for (input, absolute_path, expected) in [
1321            (
1322                r#"{"packages":[{"id":"path+file:///absolute/path/to/project/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
1323                "/path/to/zed/src/main.rs",
1324                Some(TargetInfo {
1325                    package_name: "zed".into(),
1326                    target_name: "zed".into(),
1327                    required_features: Vec::new(),
1328                    target_kind: TargetKind::Bin,
1329                }),
1330            ),
1331            (
1332                r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
1333                "/path/to/custom-package/src/main.rs",
1334                Some(TargetInfo {
1335                    package_name: "my-custom-package".into(),
1336                    target_name: "my-custom-bin".into(),
1337                    required_features: Vec::new(),
1338                    target_kind: TargetKind::Bin,
1339                }),
1340            ),
1341            (
1342                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"}]}]}"#,
1343                "/path/to/custom-package/src/main.rs",
1344                Some(TargetInfo {
1345                    package_name: "my-custom-package".into(),
1346                    target_name: "my-custom-bin".into(),
1347                    required_features: Vec::new(),
1348                    target_kind: TargetKind::Example,
1349                }),
1350            ),
1351            (
1352                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":["foo","bar"]}]}]}"#,
1353                "/path/to/custom-package/src/main.rs",
1354                Some(TargetInfo {
1355                    package_name: "my-custom-package".into(),
1356                    target_name: "my-custom-bin".into(),
1357                    required_features: vec!["foo".to_owned(), "bar".to_owned()],
1358                    target_kind: TargetKind::Example,
1359                }),
1360            ),
1361            (
1362                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":[]}]}]}"#,
1363                "/path/to/custom-package/src/main.rs",
1364                Some(TargetInfo {
1365                    package_name: "my-custom-package".into(),
1366                    target_name: "my-custom-bin".into(),
1367                    required_features: vec![],
1368                    target_kind: TargetKind::Example,
1369                }),
1370            ),
1371            (
1372                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"}]}]}"#,
1373                "/path/to/custom-package/src/main.rs",
1374                None,
1375            ),
1376        ] {
1377            let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
1378
1379            let absolute_path = Path::new(absolute_path);
1380
1381            assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1382        }
1383    }
1384
1385    #[test]
1386    fn test_rust_test_fragment() {
1387        #[track_caller]
1388        fn check(
1389            variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1390            path: &str,
1391            expected: &str,
1392        ) {
1393            let path = Path::new(path);
1394            let found = test_fragment(
1395                &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1396                path,
1397                &path.file_stem().unwrap().to_str().unwrap(),
1398            );
1399            assert_eq!(expected, found);
1400        }
1401
1402        check([], "/project/src/lib.rs", "--lib");
1403        check([], "/project/src/foo/mod.rs", "foo");
1404        check(
1405            [
1406                (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1407                (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1408            ],
1409            "/project/src/main.rs",
1410            "--bin=x",
1411        );
1412        check([], "/project/src/main.rs", "--");
1413    }
1414}