rust.rs

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