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                let filter_range = completion
 314                    .filter_text
 315                    .as_deref()
 316                    .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
 317                    .unwrap_or(0..name.len());
 318                return Some(CodeLabel {
 319                    text,
 320                    runs,
 321                    filter_range,
 322                });
 323            }
 324            (
 325                Some(detail),
 326                Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
 327            ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
 328                let name = &completion.label;
 329                let text = format!(
 330                    "{}: {}",
 331                    name,
 332                    completion.detail.as_deref().unwrap_or(detail)
 333                );
 334                let prefix = "let ";
 335                let source = Rope::from(format!("{prefix}{text} = ();"));
 336                let runs =
 337                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
 338                let filter_range = completion
 339                    .filter_text
 340                    .as_deref()
 341                    .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
 342                    .unwrap_or(0..name.len());
 343                return Some(CodeLabel {
 344                    text,
 345                    runs,
 346                    filter_range,
 347                });
 348            }
 349            (
 350                Some(detail),
 351                Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
 352            ) => {
 353                static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
 354                const FUNCTION_PREFIXES: [&str; 6] = [
 355                    "async fn",
 356                    "async unsafe fn",
 357                    "const fn",
 358                    "const unsafe fn",
 359                    "unsafe fn",
 360                    "fn",
 361                ];
 362                // Is it function `async`?
 363                let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| {
 364                    function_signature.as_ref().and_then(|signature| {
 365                        signature
 366                            .strip_prefix(*prefix)
 367                            .map(|suffix| (*prefix, suffix))
 368                    })
 369                });
 370                // fn keyword should be followed by opening parenthesis.
 371                if let Some((prefix, suffix)) = fn_keyword {
 372                    let mut text = REGEX.replace(&completion.label, suffix).to_string();
 373                    let source = Rope::from(format!("{prefix} {text} {{}}"));
 374                    let run_start = prefix.len() + 1;
 375                    let runs = language.highlight_text(&source, run_start..run_start + text.len());
 376                    if detail.starts_with("(") {
 377                        text.push(' ');
 378                        text.push_str(&detail);
 379                    }
 380                    let filter_range = completion
 381                        .filter_text
 382                        .as_deref()
 383                        .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
 384                        .unwrap_or(0..completion.label.find('(').unwrap_or(text.len()));
 385                    return Some(CodeLabel {
 386                        filter_range,
 387                        text,
 388                        runs,
 389                    });
 390                } else if completion
 391                    .detail
 392                    .as_ref()
 393                    .map_or(false, |detail| detail.starts_with("macro_rules! "))
 394                {
 395                    let text = completion.label.clone();
 396                    let len = text.len();
 397                    let source = Rope::from(text.as_str());
 398                    let runs = language.highlight_text(&source, 0..len);
 399                    let filter_range = completion
 400                        .filter_text
 401                        .as_deref()
 402                        .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
 403                        .unwrap_or(0..len);
 404                    return Some(CodeLabel {
 405                        filter_range,
 406                        text,
 407                        runs,
 408                    });
 409                }
 410            }
 411            (_, Some(kind)) => {
 412                let highlight_name = match kind {
 413                    lsp::CompletionItemKind::STRUCT
 414                    | lsp::CompletionItemKind::INTERFACE
 415                    | lsp::CompletionItemKind::ENUM => Some("type"),
 416                    lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
 417                    lsp::CompletionItemKind::KEYWORD => Some("keyword"),
 418                    lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
 419                        Some("constant")
 420                    }
 421                    _ => None,
 422                };
 423
 424                let mut label = completion.label.clone();
 425                if let Some(detail) = detail.filter(|detail| detail.starts_with("(")) {
 426                    label.push(' ');
 427                    label.push_str(detail);
 428                }
 429                let mut label = CodeLabel::plain(label, completion.filter_text.as_deref());
 430                if let Some(highlight_name) = highlight_name {
 431                    let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
 432                    label.runs.push((
 433                        0..label.text.rfind('(').unwrap_or(completion.label.len()),
 434                        highlight_id,
 435                    ));
 436                }
 437
 438                return Some(label);
 439            }
 440            _ => {}
 441        }
 442        None
 443    }
 444
 445    async fn label_for_symbol(
 446        &self,
 447        name: &str,
 448        kind: lsp::SymbolKind,
 449        language: &Arc<Language>,
 450    ) -> Option<CodeLabel> {
 451        let (text, filter_range, display_range) = match kind {
 452            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
 453                let text = format!("fn {} () {{}}", name);
 454                let filter_range = 3..3 + name.len();
 455                let display_range = 0..filter_range.end;
 456                (text, filter_range, display_range)
 457            }
 458            lsp::SymbolKind::STRUCT => {
 459                let text = format!("struct {} {{}}", name);
 460                let filter_range = 7..7 + name.len();
 461                let display_range = 0..filter_range.end;
 462                (text, filter_range, display_range)
 463            }
 464            lsp::SymbolKind::ENUM => {
 465                let text = format!("enum {} {{}}", name);
 466                let filter_range = 5..5 + name.len();
 467                let display_range = 0..filter_range.end;
 468                (text, filter_range, display_range)
 469            }
 470            lsp::SymbolKind::INTERFACE => {
 471                let text = format!("trait {} {{}}", name);
 472                let filter_range = 6..6 + name.len();
 473                let display_range = 0..filter_range.end;
 474                (text, filter_range, display_range)
 475            }
 476            lsp::SymbolKind::CONSTANT => {
 477                let text = format!("const {}: () = ();", name);
 478                let filter_range = 6..6 + name.len();
 479                let display_range = 0..filter_range.end;
 480                (text, filter_range, display_range)
 481            }
 482            lsp::SymbolKind::MODULE => {
 483                let text = format!("mod {} {{}}", name);
 484                let filter_range = 4..4 + name.len();
 485                let display_range = 0..filter_range.end;
 486                (text, filter_range, display_range)
 487            }
 488            lsp::SymbolKind::TYPE_PARAMETER => {
 489                let text = format!("type {} {{}}", name);
 490                let filter_range = 5..5 + name.len();
 491                let display_range = 0..filter_range.end;
 492                (text, filter_range, display_range)
 493            }
 494            _ => return None,
 495        };
 496
 497        Some(CodeLabel {
 498            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
 499            text: text[display_range].to_string(),
 500            filter_range,
 501        })
 502    }
 503
 504    fn prepare_initialize_params(
 505        &self,
 506        mut original: InitializeParams,
 507        cx: &App,
 508    ) -> Result<InitializeParams> {
 509        let enable_lsp_tasks = ProjectSettings::get_global(cx)
 510            .lsp
 511            .get(&SERVER_NAME)
 512            .map_or(false, |s| s.enable_lsp_tasks);
 513        if enable_lsp_tasks {
 514            let experimental = json!({
 515                "runnables": {
 516                    "kinds": [ "cargo", "shell" ],
 517                },
 518            });
 519            if let Some(original_experimental) = &mut original.capabilities.experimental {
 520                merge_json_value_into(experimental, original_experimental);
 521            } else {
 522                original.capabilities.experimental = Some(experimental);
 523            }
 524        }
 525
 526        let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx)
 527            .diagnostics
 528            .fetch_cargo_diagnostics();
 529        if cargo_diagnostics_fetched_separately {
 530            let disable_check_on_save = json!({
 531                "checkOnSave": false,
 532            });
 533            if let Some(initialization_options) = &mut original.initialization_options {
 534                merge_json_value_into(disable_check_on_save, initialization_options);
 535            } else {
 536                original.initialization_options = Some(disable_check_on_save);
 537            }
 538        }
 539
 540        Ok(original)
 541    }
 542}
 543
 544pub(crate) struct RustContextProvider;
 545
 546const RUST_PACKAGE_TASK_VARIABLE: VariableName =
 547    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
 548
 549/// The bin name corresponding to the current file in Cargo.toml
 550const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
 551    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
 552
 553/// The bin kind (bin/example) corresponding to the current file in Cargo.toml
 554const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
 555    VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
 556
 557/// The flag to list required features for executing a bin, if any
 558const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
 559    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
 560
 561/// The list of required features for executing a bin, if any
 562const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
 563    VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
 564
 565const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
 566    VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
 567
 568const RUST_DOC_TEST_NAME_TASK_VARIABLE: VariableName =
 569    VariableName::Custom(Cow::Borrowed("RUST_DOC_TEST_NAME"));
 570
 571const RUST_TEST_NAME_TASK_VARIABLE: VariableName =
 572    VariableName::Custom(Cow::Borrowed("RUST_TEST_NAME"));
 573
 574impl ContextProvider for RustContextProvider {
 575    fn build_context(
 576        &self,
 577        task_variables: &TaskVariables,
 578        location: ContextLocation<'_>,
 579        project_env: Option<HashMap<String, String>>,
 580        _: Arc<dyn LanguageToolchainStore>,
 581        cx: &mut gpui::App,
 582    ) -> Task<Result<TaskVariables>> {
 583        let local_abs_path = location
 584            .file_location
 585            .buffer
 586            .read(cx)
 587            .file()
 588            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 589
 590        let mut variables = TaskVariables::default();
 591
 592        if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
 593        {
 594            let fragment = test_fragment(&variables, &path, stem);
 595            variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
 596        };
 597        if let Some(test_name) =
 598            task_variables.get(&VariableName::Custom(Cow::Borrowed("_test_name")))
 599        {
 600            variables.insert(RUST_TEST_NAME_TASK_VARIABLE, test_name.into());
 601        }
 602        if let Some(doc_test_name) =
 603            task_variables.get(&VariableName::Custom(Cow::Borrowed("_doc_test_name")))
 604        {
 605            variables.insert(RUST_DOC_TEST_NAME_TASK_VARIABLE, doc_test_name.into());
 606        }
 607        cx.background_spawn(async move {
 608            if let Some(path) = local_abs_path
 609                .as_deref()
 610                .and_then(|local_abs_path| local_abs_path.parent())
 611            {
 612                if let Some(package_name) =
 613                    human_readable_package_name(path, project_env.as_ref()).await
 614                {
 615                    variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
 616                }
 617            }
 618            if let Some(path) = local_abs_path.as_ref() {
 619                if let Some(target) = target_info_from_abs_path(&path, project_env.as_ref()).await {
 620                    variables.extend(TaskVariables::from_iter([
 621                        (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
 622                        (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name),
 623                        (
 624                            RUST_BIN_KIND_TASK_VARIABLE.clone(),
 625                            target.target_kind.to_string(),
 626                        ),
 627                    ]));
 628                    if target.required_features.is_empty() {
 629                        variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
 630                        variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
 631                    } else {
 632                        variables.insert(
 633                            RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
 634                            "--features".to_string(),
 635                        );
 636                        variables.insert(
 637                            RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
 638                            target.required_features.join(","),
 639                        );
 640                    }
 641                }
 642            }
 643            Ok(variables)
 644        })
 645    }
 646
 647    fn associated_tasks(
 648        &self,
 649        _: Arc<dyn Fs>,
 650        file: Option<Arc<dyn language::File>>,
 651        cx: &App,
 652    ) -> Task<Option<TaskTemplates>> {
 653        const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
 654        const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
 655
 656        let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
 657        let package_to_run = language_sets
 658            .tasks
 659            .variables
 660            .get(DEFAULT_RUN_NAME_STR)
 661            .cloned();
 662        let custom_target_dir = language_sets
 663            .tasks
 664            .variables
 665            .get(CUSTOM_TARGET_DIR)
 666            .cloned();
 667        let run_task_args = if let Some(package_to_run) = package_to_run.clone() {
 668            vec!["run".into(), "-p".into(), package_to_run]
 669        } else {
 670            vec!["run".into()]
 671        };
 672        let mut task_templates = vec![
 673            TaskTemplate {
 674                label: format!(
 675                    "Check (package: {})",
 676                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 677                ),
 678                command: "cargo".into(),
 679                args: vec![
 680                    "check".into(),
 681                    "-p".into(),
 682                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 683                ],
 684                cwd: Some("$ZED_DIRNAME".to_owned()),
 685                ..TaskTemplate::default()
 686            },
 687            TaskTemplate {
 688                label: "Check all targets (workspace)".into(),
 689                command: "cargo".into(),
 690                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
 691                cwd: Some("$ZED_DIRNAME".to_owned()),
 692                ..TaskTemplate::default()
 693            },
 694            TaskTemplate {
 695                label: format!(
 696                    "Test '{}' (package: {})",
 697                    RUST_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                    "-p".into(),
 704                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 705                    "--".into(),
 706                    "--nocapture".into(),
 707                    "--include-ignored".into(),
 708                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
 709                ],
 710                tags: vec!["rust-test".to_owned()],
 711                cwd: Some("$ZED_DIRNAME".to_owned()),
 712                ..TaskTemplate::default()
 713            },
 714            TaskTemplate {
 715                label: format!(
 716                    "Doc test '{}' (package: {})",
 717                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 718                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 719                ),
 720                command: "cargo".into(),
 721                args: vec![
 722                    "test".into(),
 723                    "--doc".into(),
 724                    "-p".into(),
 725                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 726                    "--".into(),
 727                    "--nocapture".into(),
 728                    "--include-ignored".into(),
 729                    RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
 730                ],
 731                tags: vec!["rust-doc-test".to_owned()],
 732                cwd: Some("$ZED_DIRNAME".to_owned()),
 733                ..TaskTemplate::default()
 734            },
 735            TaskTemplate {
 736                label: format!(
 737                    "Test mod '{}' (package: {})",
 738                    VariableName::Stem.template_value(),
 739                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 740                ),
 741                command: "cargo".into(),
 742                args: vec![
 743                    "test".into(),
 744                    "-p".into(),
 745                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 746                    "--".into(),
 747                    RUST_TEST_FRAGMENT_TASK_VARIABLE.template_value(),
 748                ],
 749                tags: vec!["rust-mod-test".to_owned()],
 750                cwd: Some("$ZED_DIRNAME".to_owned()),
 751                ..TaskTemplate::default()
 752            },
 753            TaskTemplate {
 754                label: format!(
 755                    "Run {} {} (package: {})",
 756                    RUST_BIN_KIND_TASK_VARIABLE.template_value(),
 757                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 758                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 759                ),
 760                command: "cargo".into(),
 761                args: vec![
 762                    "run".into(),
 763                    "-p".into(),
 764                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 765                    format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
 766                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 767                    RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
 768                    RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
 769                ],
 770                cwd: Some("$ZED_DIRNAME".to_owned()),
 771                tags: vec!["rust-main".to_owned()],
 772                ..TaskTemplate::default()
 773            },
 774            TaskTemplate {
 775                label: format!(
 776                    "Test (package: {})",
 777                    RUST_PACKAGE_TASK_VARIABLE.template_value()
 778                ),
 779                command: "cargo".into(),
 780                args: vec![
 781                    "test".into(),
 782                    "-p".into(),
 783                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 784                ],
 785                cwd: Some("$ZED_DIRNAME".to_owned()),
 786                ..TaskTemplate::default()
 787            },
 788            TaskTemplate {
 789                label: "Run".into(),
 790                command: "cargo".into(),
 791                args: run_task_args,
 792                cwd: Some("$ZED_DIRNAME".to_owned()),
 793                ..TaskTemplate::default()
 794            },
 795            TaskTemplate {
 796                label: "Clean".into(),
 797                command: "cargo".into(),
 798                args: vec!["clean".into()],
 799                cwd: Some("$ZED_DIRNAME".to_owned()),
 800                ..TaskTemplate::default()
 801            },
 802        ];
 803
 804        if let Some(custom_target_dir) = custom_target_dir {
 805            task_templates = task_templates
 806                .into_iter()
 807                .map(|mut task_template| {
 808                    let mut args = task_template.args.split_off(1);
 809                    task_template.args.append(&mut vec![
 810                        "--target-dir".to_string(),
 811                        custom_target_dir.clone(),
 812                    ]);
 813                    task_template.args.append(&mut args);
 814
 815                    task_template
 816                })
 817                .collect();
 818        }
 819
 820        Task::ready(Some(TaskTemplates(task_templates)))
 821    }
 822
 823    fn lsp_task_source(&self) -> Option<LanguageServerName> {
 824        Some(SERVER_NAME)
 825    }
 826}
 827
 828/// Part of the data structure of Cargo metadata
 829#[derive(serde::Deserialize)]
 830struct CargoMetadata {
 831    packages: Vec<CargoPackage>,
 832}
 833
 834#[derive(serde::Deserialize)]
 835struct CargoPackage {
 836    id: String,
 837    targets: Vec<CargoTarget>,
 838}
 839
 840#[derive(serde::Deserialize)]
 841struct CargoTarget {
 842    name: String,
 843    kind: Vec<String>,
 844    src_path: String,
 845    #[serde(rename = "required-features", default)]
 846    required_features: Vec<String>,
 847}
 848
 849#[derive(Debug, PartialEq)]
 850enum TargetKind {
 851    Bin,
 852    Example,
 853}
 854
 855impl Display for TargetKind {
 856    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 857        match self {
 858            TargetKind::Bin => write!(f, "bin"),
 859            TargetKind::Example => write!(f, "example"),
 860        }
 861    }
 862}
 863
 864impl TryFrom<&str> for TargetKind {
 865    type Error = ();
 866    fn try_from(value: &str) -> Result<Self, ()> {
 867        match value {
 868            "bin" => Ok(Self::Bin),
 869            "example" => Ok(Self::Example),
 870            _ => Err(()),
 871        }
 872    }
 873}
 874/// Which package and binary target are we in?
 875#[derive(Debug, PartialEq)]
 876struct TargetInfo {
 877    package_name: String,
 878    target_name: String,
 879    target_kind: TargetKind,
 880    required_features: Vec<String>,
 881}
 882
 883async fn target_info_from_abs_path(
 884    abs_path: &Path,
 885    project_env: Option<&HashMap<String, String>>,
 886) -> Option<TargetInfo> {
 887    let mut command = util::command::new_smol_command("cargo");
 888    if let Some(envs) = project_env {
 889        command.envs(envs);
 890    }
 891    let output = command
 892        .current_dir(abs_path.parent()?)
 893        .arg("metadata")
 894        .arg("--no-deps")
 895        .arg("--format-version")
 896        .arg("1")
 897        .output()
 898        .await
 899        .log_err()?
 900        .stdout;
 901
 902    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
 903
 904    target_info_from_metadata(metadata, abs_path)
 905}
 906
 907fn target_info_from_metadata(metadata: CargoMetadata, abs_path: &Path) -> Option<TargetInfo> {
 908    for package in metadata.packages {
 909        for target in package.targets {
 910            let Some(bin_kind) = target
 911                .kind
 912                .iter()
 913                .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok())
 914            else {
 915                continue;
 916            };
 917            let target_path = PathBuf::from(target.src_path);
 918            if target_path == abs_path {
 919                return package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
 920                    package_name: package_name.to_owned(),
 921                    target_name: target.name,
 922                    required_features: target.required_features,
 923                    target_kind: bin_kind,
 924                });
 925            }
 926        }
 927    }
 928
 929    None
 930}
 931
 932async fn human_readable_package_name(
 933    package_directory: &Path,
 934    project_env: Option<&HashMap<String, String>>,
 935) -> Option<String> {
 936    let mut command = util::command::new_smol_command("cargo");
 937    if let Some(envs) = project_env {
 938        command.envs(envs);
 939    }
 940    let pkgid = String::from_utf8(
 941        command
 942            .current_dir(package_directory)
 943            .arg("pkgid")
 944            .output()
 945            .await
 946            .log_err()?
 947            .stdout,
 948    )
 949    .ok()?;
 950    Some(package_name_from_pkgid(&pkgid)?.to_owned())
 951}
 952
 953// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
 954// Output example in the root of Zed project:
 955// ```sh
 956// ❯ cargo pkgid zed
 957// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
 958// ```
 959// Another variant, if a project has a custom package name or hyphen in the name:
 960// ```
 961// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
 962// ```
 963//
 964// Extracts the package name from the output according to the spec:
 965// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
 966fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
 967    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
 968        match input.rsplit_once(suffix_start) {
 969            Some((without_suffix, _)) => without_suffix,
 970            None => input,
 971        }
 972    }
 973
 974    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
 975    let package_name = match version_suffix.rsplit_once('@') {
 976        Some((custom_package_name, _version)) => custom_package_name,
 977        None => {
 978            let host_and_path = split_off_suffix(version_prefix, '?');
 979            let (_, package_name) = host_and_path.rsplit_once('/')?;
 980            package_name
 981        }
 982    };
 983    Some(package_name)
 984}
 985
 986async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
 987    maybe!(async {
 988        let mut last = None;
 989        let mut entries = fs::read_dir(&container_dir).await?;
 990        while let Some(entry) = entries.next().await {
 991            last = Some(entry?.path());
 992        }
 993
 994        anyhow::Ok(LanguageServerBinary {
 995            path: last.context("no cached binary")?,
 996            env: None,
 997            arguments: Default::default(),
 998        })
 999    })
1000    .await
1001    .log_err()
1002}
1003
1004fn test_fragment(variables: &TaskVariables, path: &Path, stem: &str) -> String {
1005    let fragment = if stem == "lib" {
1006        // This isn't quite right---it runs the tests for the entire library, rather than
1007        // just for the top-level `mod tests`. But we don't really have the means here to
1008        // filter out just that module.
1009        Some("--lib".to_owned())
1010    } else if stem == "mod" {
1011        maybe!({ Some(path.parent()?.file_name()?.to_string_lossy().to_string()) })
1012    } else if stem == "main" {
1013        if let (Some(bin_name), Some(bin_kind)) = (
1014            variables.get(&RUST_BIN_NAME_TASK_VARIABLE),
1015            variables.get(&RUST_BIN_KIND_TASK_VARIABLE),
1016        ) {
1017            Some(format!("--{bin_kind}={bin_name}"))
1018        } else {
1019            None
1020        }
1021    } else {
1022        Some(stem.to_owned())
1023    };
1024    fragment.unwrap_or_else(|| "--".to_owned())
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use std::num::NonZeroU32;
1030
1031    use super::*;
1032    use crate::language;
1033    use gpui::{BorrowAppContext, Hsla, TestAppContext};
1034    use language::language_settings::AllLanguageSettings;
1035    use lsp::CompletionItemLabelDetails;
1036    use settings::SettingsStore;
1037    use theme::SyntaxTheme;
1038    use util::path;
1039
1040    #[gpui::test]
1041    async fn test_process_rust_diagnostics() {
1042        let mut params = lsp::PublishDiagnosticsParams {
1043            uri: lsp::Url::from_file_path(path!("/a")).unwrap(),
1044            version: None,
1045            diagnostics: vec![
1046                // no newlines
1047                lsp::Diagnostic {
1048                    message: "use of moved value `a`".to_string(),
1049                    ..Default::default()
1050                },
1051                // newline at the end of a code span
1052                lsp::Diagnostic {
1053                    message: "consider importing this struct: `use b::c;\n`".to_string(),
1054                    ..Default::default()
1055                },
1056                // code span starting right after a newline
1057                lsp::Diagnostic {
1058                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1059                        .to_string(),
1060                    ..Default::default()
1061                },
1062            ],
1063        };
1064        RustLspAdapter.process_diagnostics(&mut params, LanguageServerId(0), None);
1065
1066        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
1067
1068        // remove trailing newline from code span
1069        assert_eq!(
1070            params.diagnostics[1].message,
1071            "consider importing this struct: `use b::c;`"
1072        );
1073
1074        // do not remove newline before the start of code span
1075        assert_eq!(
1076            params.diagnostics[2].message,
1077            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
1078        );
1079    }
1080
1081    #[gpui::test]
1082    async fn test_rust_label_for_completion() {
1083        let adapter = Arc::new(RustLspAdapter);
1084        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1085        let grammar = language.grammar().unwrap();
1086        let theme = SyntaxTheme::new_test([
1087            ("type", Hsla::default()),
1088            ("keyword", Hsla::default()),
1089            ("function", Hsla::default()),
1090            ("property", Hsla::default()),
1091        ]);
1092
1093        language.set_theme(&theme);
1094
1095        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1096        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1097        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1098        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
1099
1100        assert_eq!(
1101            adapter
1102                .label_for_completion(
1103                    &lsp::CompletionItem {
1104                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1105                        label: "hello(…)".to_string(),
1106                        label_details: Some(CompletionItemLabelDetails {
1107                            detail: Some("(use crate::foo)".into()),
1108                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
1109                        }),
1110                        ..Default::default()
1111                    },
1112                    &language
1113                )
1114                .await,
1115            Some(CodeLabel {
1116                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1117                filter_range: 0..5,
1118                runs: vec![
1119                    (0..5, highlight_function),
1120                    (7..10, highlight_keyword),
1121                    (11..17, highlight_type),
1122                    (18..19, highlight_type),
1123                    (25..28, highlight_type),
1124                    (29..30, highlight_type),
1125                ],
1126            })
1127        );
1128        assert_eq!(
1129            adapter
1130                .label_for_completion(
1131                    &lsp::CompletionItem {
1132                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1133                        label: "hello(…)".to_string(),
1134                        label_details: Some(CompletionItemLabelDetails {
1135                            detail: Some(" (use crate::foo)".into()),
1136                            description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
1137                        }),
1138                        ..Default::default()
1139                    },
1140                    &language
1141                )
1142                .await,
1143            Some(CodeLabel {
1144                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1145                filter_range: 0..5,
1146                runs: vec![
1147                    (0..5, highlight_function),
1148                    (7..10, highlight_keyword),
1149                    (11..17, highlight_type),
1150                    (18..19, highlight_type),
1151                    (25..28, highlight_type),
1152                    (29..30, highlight_type),
1153                ],
1154            })
1155        );
1156        assert_eq!(
1157            adapter
1158                .label_for_completion(
1159                    &lsp::CompletionItem {
1160                        kind: Some(lsp::CompletionItemKind::FIELD),
1161                        label: "len".to_string(),
1162                        detail: Some("usize".to_string()),
1163                        ..Default::default()
1164                    },
1165                    &language
1166                )
1167                .await,
1168            Some(CodeLabel {
1169                text: "len: usize".to_string(),
1170                filter_range: 0..3,
1171                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
1172            })
1173        );
1174
1175        assert_eq!(
1176            adapter
1177                .label_for_completion(
1178                    &lsp::CompletionItem {
1179                        kind: Some(lsp::CompletionItemKind::FUNCTION),
1180                        label: "hello(…)".to_string(),
1181                        label_details: Some(CompletionItemLabelDetails {
1182                            detail: Some(" (use crate::foo)".to_string()),
1183                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
1184                        }),
1185
1186                        ..Default::default()
1187                    },
1188                    &language
1189                )
1190                .await,
1191            Some(CodeLabel {
1192                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
1193                filter_range: 0..5,
1194                runs: vec![
1195                    (0..5, highlight_function),
1196                    (7..10, highlight_keyword),
1197                    (11..17, highlight_type),
1198                    (18..19, highlight_type),
1199                    (25..28, highlight_type),
1200                    (29..30, highlight_type),
1201                ],
1202            })
1203        );
1204
1205        assert_eq!(
1206            adapter
1207                .label_for_completion(
1208                    &lsp::CompletionItem {
1209                        kind: Some(lsp::CompletionItemKind::METHOD),
1210                        label: "await.as_deref_mut()".to_string(),
1211                        filter_text: Some("as_deref_mut".to_string()),
1212                        label_details: Some(CompletionItemLabelDetails {
1213                            detail: None,
1214                            description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
1215                        }),
1216                        ..Default::default()
1217                    },
1218                    &language
1219                )
1220                .await,
1221            Some(CodeLabel {
1222                text: "await.as_deref_mut()".to_string(),
1223                filter_range: 6..18,
1224                runs: vec![],
1225            })
1226        );
1227
1228        assert_eq!(
1229            adapter
1230                .label_for_completion(
1231                    &lsp::CompletionItem {
1232                        kind: Some(lsp::CompletionItemKind::FIELD),
1233                        label: "inner_value".to_string(),
1234                        filter_text: Some("value".to_string()),
1235                        detail: Some("String".to_string()),
1236                        ..Default::default()
1237                    },
1238                    &language,
1239                )
1240                .await,
1241            Some(CodeLabel {
1242                text: "inner_value: String".to_string(),
1243                filter_range: 6..11,
1244                runs: vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
1245            })
1246        );
1247    }
1248
1249    #[gpui::test]
1250    async fn test_rust_label_for_symbol() {
1251        let adapter = Arc::new(RustLspAdapter);
1252        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
1253        let grammar = language.grammar().unwrap();
1254        let theme = SyntaxTheme::new_test([
1255            ("type", Hsla::default()),
1256            ("keyword", Hsla::default()),
1257            ("function", Hsla::default()),
1258            ("property", Hsla::default()),
1259        ]);
1260
1261        language.set_theme(&theme);
1262
1263        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
1264        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
1265        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
1266
1267        assert_eq!(
1268            adapter
1269                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
1270                .await,
1271            Some(CodeLabel {
1272                text: "fn hello".to_string(),
1273                filter_range: 3..8,
1274                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
1275            })
1276        );
1277
1278        assert_eq!(
1279            adapter
1280                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
1281                .await,
1282            Some(CodeLabel {
1283                text: "type World".to_string(),
1284                filter_range: 5..10,
1285                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
1286            })
1287        );
1288    }
1289
1290    #[gpui::test]
1291    async fn test_rust_autoindent(cx: &mut TestAppContext) {
1292        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1293        cx.update(|cx| {
1294            let test_settings = SettingsStore::test(cx);
1295            cx.set_global(test_settings);
1296            language::init(cx);
1297            cx.update_global::<SettingsStore, _>(|store, cx| {
1298                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
1299                    s.defaults.tab_size = NonZeroU32::new(2);
1300                });
1301            });
1302        });
1303
1304        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
1305
1306        cx.new(|cx| {
1307            let mut buffer = Buffer::local("", cx).with_language(language, cx);
1308
1309            // indent between braces
1310            buffer.set_text("fn a() {}", cx);
1311            let ix = buffer.len() - 1;
1312            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1313            assert_eq!(buffer.text(), "fn a() {\n  \n}");
1314
1315            // indent between braces, even after empty lines
1316            buffer.set_text("fn a() {\n\n\n}", cx);
1317            let ix = buffer.len() - 2;
1318            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1319            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1320
1321            // indent a line that continues a field expression
1322            buffer.set_text("fn a() {\n  \n}", cx);
1323            let ix = buffer.len() - 2;
1324            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1325            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1326
1327            // indent further lines that continue the field expression, even after empty lines
1328            let ix = buffer.len() - 2;
1329            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1330            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1331
1332            // dedent the line after the field expression
1333            let ix = buffer.len() - 2;
1334            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1335            assert_eq!(
1336                buffer.text(),
1337                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1338            );
1339
1340            // indent inside a struct within a call
1341            buffer.set_text("const a: B = c(D {});", cx);
1342            let ix = buffer.len() - 3;
1343            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1344            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1345
1346            // indent further inside a nested call
1347            let ix = buffer.len() - 4;
1348            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1349            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1350
1351            // keep that indent after an empty line
1352            let ix = buffer.len() - 8;
1353            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1354            assert_eq!(
1355                buffer.text(),
1356                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1357            );
1358
1359            buffer
1360        });
1361    }
1362
1363    #[test]
1364    fn test_package_name_from_pkgid() {
1365        for (input, expected) in [
1366            (
1367                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1368                "zed",
1369            ),
1370            (
1371                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1372                "my-custom-package",
1373            ),
1374        ] {
1375            assert_eq!(package_name_from_pkgid(input), Some(expected));
1376        }
1377    }
1378
1379    #[test]
1380    fn test_target_info_from_metadata() {
1381        for (input, absolute_path, expected) in [
1382            (
1383                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"}]}]}"#,
1384                "/path/to/zed/src/main.rs",
1385                Some(TargetInfo {
1386                    package_name: "zed".into(),
1387                    target_name: "zed".into(),
1388                    required_features: Vec::new(),
1389                    target_kind: TargetKind::Bin,
1390                }),
1391            ),
1392            (
1393                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"}]}]}"#,
1394                "/path/to/custom-package/src/main.rs",
1395                Some(TargetInfo {
1396                    package_name: "my-custom-package".into(),
1397                    target_name: "my-custom-bin".into(),
1398                    required_features: Vec::new(),
1399                    target_kind: TargetKind::Bin,
1400                }),
1401            ),
1402            (
1403                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"}]}]}"#,
1404                "/path/to/custom-package/src/main.rs",
1405                Some(TargetInfo {
1406                    package_name: "my-custom-package".into(),
1407                    target_name: "my-custom-bin".into(),
1408                    required_features: Vec::new(),
1409                    target_kind: TargetKind::Example,
1410                }),
1411            ),
1412            (
1413                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"]}]}]}"#,
1414                "/path/to/custom-package/src/main.rs",
1415                Some(TargetInfo {
1416                    package_name: "my-custom-package".into(),
1417                    target_name: "my-custom-bin".into(),
1418                    required_features: vec!["foo".to_owned(), "bar".to_owned()],
1419                    target_kind: TargetKind::Example,
1420                }),
1421            ),
1422            (
1423                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":[]}]}]}"#,
1424                "/path/to/custom-package/src/main.rs",
1425                Some(TargetInfo {
1426                    package_name: "my-custom-package".into(),
1427                    target_name: "my-custom-bin".into(),
1428                    required_features: vec![],
1429                    target_kind: TargetKind::Example,
1430                }),
1431            ),
1432            (
1433                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"}]}]}"#,
1434                "/path/to/custom-package/src/main.rs",
1435                None,
1436            ),
1437        ] {
1438            let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
1439
1440            let absolute_path = Path::new(absolute_path);
1441
1442            assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
1443        }
1444    }
1445
1446    #[test]
1447    fn test_rust_test_fragment() {
1448        #[track_caller]
1449        fn check(
1450            variables: impl IntoIterator<Item = (VariableName, &'static str)>,
1451            path: &str,
1452            expected: &str,
1453        ) {
1454            let path = Path::new(path);
1455            let found = test_fragment(
1456                &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
1457                path,
1458                &path.file_stem().unwrap().to_str().unwrap(),
1459            );
1460            assert_eq!(expected, found);
1461        }
1462
1463        check([], "/project/src/lib.rs", "--lib");
1464        check([], "/project/src/foo/mod.rs", "foo");
1465        check(
1466            [
1467                (RUST_BIN_KIND_TASK_VARIABLE.clone(), "bin"),
1468                (RUST_BIN_NAME_TASK_VARIABLE, "x"),
1469            ],
1470            "/project/src/main.rs",
1471            "--bin=x",
1472        );
1473        check([], "/project/src/main.rs", "--");
1474    }
1475}