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