rust.rs

   1use anyhow::{anyhow, Context, Result};
   2use async_compression::futures::bufread::GzipDecoder;
   3use async_trait::async_trait;
   4use collections::HashMap;
   5use futures::{io::BufReader, StreamExt};
   6use gpui::{AppContext, AsyncAppContext, Task};
   7use http_client::github::AssetKind;
   8use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
   9pub use language::*;
  10use lsp::{LanguageServerBinary, LanguageServerName};
  11use regex::Regex;
  12use smol::fs::{self};
  13use std::{
  14    any::Any,
  15    borrow::Cow,
  16    path::{Path, PathBuf},
  17    sync::Arc,
  18    sync::LazyLock,
  19};
  20use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
  21use util::{fs::remove_matching, maybe, ResultExt};
  22
  23use crate::language_settings::language_settings;
  24
  25pub struct RustLspAdapter;
  26
  27#[cfg(target_os = "macos")]
  28impl RustLspAdapter {
  29    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  30    const ARCH_SERVER_NAME: &str = "apple-darwin";
  31}
  32
  33#[cfg(target_os = "linux")]
  34impl RustLspAdapter {
  35    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  36    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
  37}
  38
  39#[cfg(target_os = "freebsd")]
  40impl RustLspAdapter {
  41    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  42    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
  43}
  44
  45#[cfg(target_os = "windows")]
  46impl RustLspAdapter {
  47    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
  48    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
  49}
  50
  51impl RustLspAdapter {
  52    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
  53
  54    fn build_asset_name() -> String {
  55        let extension = match Self::GITHUB_ASSET_KIND {
  56            AssetKind::TarGz => "tar.gz",
  57            AssetKind::Gz => "gz",
  58            AssetKind::Zip => "zip",
  59        };
  60
  61        format!(
  62            "{}-{}-{}.{}",
  63            Self::SERVER_NAME,
  64            std::env::consts::ARCH,
  65            Self::ARCH_SERVER_NAME,
  66            extension
  67        )
  68    }
  69}
  70
  71#[async_trait(?Send)]
  72impl LspAdapter for RustLspAdapter {
  73    fn name(&self) -> LanguageServerName {
  74        Self::SERVER_NAME.clone()
  75    }
  76
  77    async fn check_if_user_installed(
  78        &self,
  79        delegate: &dyn LspAdapterDelegate,
  80        _: &AsyncAppContext,
  81    ) -> Option<LanguageServerBinary> {
  82        let path = delegate.which("rust-analyzer".as_ref()).await?;
  83        let env = delegate.shell_env().await;
  84
  85        // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
  86        // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
  87        log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
  88        let result = delegate
  89            .try_exec(LanguageServerBinary {
  90                path: path.clone(),
  91                arguments: vec!["--help".into()],
  92                env: Some(env.clone()),
  93            })
  94            .await;
  95        if let Err(err) = result {
  96            log::error!(
  97                "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
  98                path,
  99                err
 100            );
 101            return None;
 102        }
 103
 104        Some(LanguageServerBinary {
 105            path,
 106            env: Some(env),
 107            arguments: vec![],
 108        })
 109    }
 110
 111    async fn fetch_latest_server_version(
 112        &self,
 113        delegate: &dyn LspAdapterDelegate,
 114    ) -> Result<Box<dyn 'static + Send + Any>> {
 115        let release = latest_github_release(
 116            "rust-lang/rust-analyzer",
 117            true,
 118            false,
 119            delegate.http_client(),
 120        )
 121        .await?;
 122        let asset_name = Self::build_asset_name();
 123
 124        let asset = release
 125            .assets
 126            .iter()
 127            .find(|asset| asset.name == asset_name)
 128            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 129        Ok(Box::new(GitHubLspBinaryVersion {
 130            name: release.tag_name,
 131            url: asset.browser_download_url.clone(),
 132        }))
 133    }
 134
 135    async fn fetch_server_binary(
 136        &self,
 137        version: Box<dyn 'static + Send + Any>,
 138        container_dir: PathBuf,
 139        delegate: &dyn LspAdapterDelegate,
 140    ) -> Result<LanguageServerBinary> {
 141        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 142        let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 143        let server_path = match Self::GITHUB_ASSET_KIND {
 144            AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
 145            AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe
 146        };
 147
 148        if fs::metadata(&server_path).await.is_err() {
 149            remove_matching(&container_dir, |entry| entry != destination_path).await;
 150
 151            let mut response = delegate
 152                .http_client()
 153                .get(&version.url, Default::default(), true)
 154                .await
 155                .with_context(|| format!("downloading release from {}", version.url))?;
 156            match Self::GITHUB_ASSET_KIND {
 157                AssetKind::TarGz => {
 158                    let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
 159                    let archive = async_tar::Archive::new(decompressed_bytes);
 160                    archive.unpack(&destination_path).await.with_context(|| {
 161                        format!("extracting {} to {:?}", version.url, destination_path)
 162                    })?;
 163                }
 164                AssetKind::Gz => {
 165                    let mut decompressed_bytes =
 166                        GzipDecoder::new(BufReader::new(response.body_mut()));
 167                    let mut file =
 168                        fs::File::create(&destination_path).await.with_context(|| {
 169                            format!(
 170                                "creating a file {:?} for a download from {}",
 171                                destination_path, version.url,
 172                            )
 173                        })?;
 174                    futures::io::copy(&mut decompressed_bytes, &mut file)
 175                        .await
 176                        .with_context(|| {
 177                            format!("extracting {} to {:?}", version.url, destination_path)
 178                        })?;
 179                }
 180                AssetKind::Zip => {
 181                    node_runtime::extract_zip(
 182                        &destination_path,
 183                        BufReader::new(response.body_mut()),
 184                    )
 185                    .await
 186                    .with_context(|| {
 187                        format!("unzipping {} to {:?}", version.url, destination_path)
 188                    })?;
 189                }
 190            };
 191
 192            // todo("windows")
 193            #[cfg(not(windows))]
 194            {
 195                fs::set_permissions(
 196                    &server_path,
 197                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
 198                )
 199                .await?;
 200            }
 201        }
 202
 203        Ok(LanguageServerBinary {
 204            path: server_path,
 205            env: None,
 206            arguments: Default::default(),
 207        })
 208    }
 209
 210    async fn cached_server_binary(
 211        &self,
 212        container_dir: PathBuf,
 213        _: &dyn LspAdapterDelegate,
 214    ) -> Option<LanguageServerBinary> {
 215        get_cached_server_binary(container_dir).await
 216    }
 217
 218    fn disk_based_diagnostic_sources(&self) -> Vec<String> {
 219        vec!["rustc".into()]
 220    }
 221
 222    fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
 223        Some("rust-analyzer/flycheck".into())
 224    }
 225
 226    fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
 227        static REGEX: LazyLock<Regex> =
 228            LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
 229
 230        for diagnostic in &mut params.diagnostics {
 231            for message in diagnostic
 232                .related_information
 233                .iter_mut()
 234                .flatten()
 235                .map(|info| &mut info.message)
 236                .chain([&mut diagnostic.message])
 237            {
 238                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
 239                    *message = sanitized;
 240                }
 241            }
 242        }
 243    }
 244
 245    async fn label_for_completion(
 246        &self,
 247        completion: &lsp::CompletionItem,
 248        language: &Arc<Language>,
 249    ) -> Option<CodeLabel> {
 250        let detail = completion
 251            .label_details
 252            .as_ref()
 253            .and_then(|detail| detail.detail.as_ref())
 254            .or(completion.detail.as_ref())
 255            .map(ToOwned::to_owned);
 256        let function_signature = completion
 257            .label_details
 258            .as_ref()
 259            .and_then(|detail| detail.description.as_ref())
 260            .or(completion.detail.as_ref())
 261            .map(ToOwned::to_owned);
 262        match completion.kind {
 263            Some(lsp::CompletionItemKind::FIELD) if detail.is_some() => {
 264                let name = &completion.label;
 265                let text = format!("{}: {}", name, detail.unwrap());
 266                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
 267                let runs = language.highlight_text(&source, 11..11 + text.len());
 268                return Some(CodeLabel {
 269                    text,
 270                    runs,
 271                    filter_range: 0..name.len(),
 272                });
 273            }
 274            Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
 275                if detail.is_some()
 276                    && completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
 277            {
 278                let name = &completion.label;
 279                let text = format!(
 280                    "{}: {}",
 281                    name,
 282                    completion.detail.as_ref().or(detail.as_ref()).unwrap()
 283                );
 284                let source = Rope::from(format!("let {} = ();", text).as_str());
 285                let runs = language.highlight_text(&source, 4..4 + text.len());
 286                return Some(CodeLabel {
 287                    text,
 288                    runs,
 289                    filter_range: 0..name.len(),
 290                });
 291            }
 292            Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
 293                if detail.is_some() =>
 294            {
 295                static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
 296
 297                let detail = detail.unwrap();
 298                const FUNCTION_PREFIXES: [&str; 6] = [
 299                    "async fn",
 300                    "async unsafe fn",
 301                    "const fn",
 302                    "const unsafe fn",
 303                    "unsafe fn",
 304                    "fn",
 305                ];
 306                // Is it function `async`?
 307                let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| {
 308                    function_signature.as_ref().and_then(|signature| {
 309                        signature
 310                            .strip_prefix(*prefix)
 311                            .map(|suffix| (*prefix, suffix))
 312                    })
 313                });
 314                // fn keyword should be followed by opening parenthesis.
 315                if let Some((prefix, suffix)) = fn_keyword {
 316                    let mut text = REGEX.replace(&completion.label, suffix).to_string();
 317                    let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
 318                    let run_start = prefix.len() + 1;
 319                    let runs = language.highlight_text(&source, run_start..run_start + text.len());
 320                    if detail.starts_with(" (") {
 321                        text.push_str(&detail);
 322                    }
 323
 324                    return Some(CodeLabel {
 325                        filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
 326                        text,
 327                        runs,
 328                    });
 329                } else if completion
 330                    .detail
 331                    .as_ref()
 332                    .map_or(false, |detail| detail.starts_with("macro_rules! "))
 333                {
 334                    let source = Rope::from(completion.label.as_str());
 335                    let runs = language.highlight_text(&source, 0..completion.label.len());
 336
 337                    return Some(CodeLabel {
 338                        filter_range: 0..completion.label.len(),
 339                        text: completion.label.clone(),
 340                        runs,
 341                    });
 342                }
 343            }
 344            Some(kind) => {
 345                let highlight_name = match kind {
 346                    lsp::CompletionItemKind::STRUCT
 347                    | lsp::CompletionItemKind::INTERFACE
 348                    | lsp::CompletionItemKind::ENUM => Some("type"),
 349                    lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
 350                    lsp::CompletionItemKind::KEYWORD => Some("keyword"),
 351                    lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
 352                        Some("constant")
 353                    }
 354                    _ => None,
 355                };
 356
 357                let mut label = completion.label.clone();
 358                if let Some(detail) = detail.filter(|detail| detail.starts_with(" (")) {
 359                    use std::fmt::Write;
 360                    write!(label, "{detail}").ok()?;
 361                }
 362                let mut label = CodeLabel::plain(label, None);
 363                if let Some(highlight_name) = highlight_name {
 364                    let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
 365                    label.runs.push((
 366                        0..label.text.rfind('(').unwrap_or(completion.label.len()),
 367                        highlight_id,
 368                    ));
 369                }
 370
 371                return Some(label);
 372            }
 373            _ => {}
 374        }
 375        None
 376    }
 377
 378    async fn label_for_symbol(
 379        &self,
 380        name: &str,
 381        kind: lsp::SymbolKind,
 382        language: &Arc<Language>,
 383    ) -> Option<CodeLabel> {
 384        let (text, filter_range, display_range) = match kind {
 385            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
 386                let text = format!("fn {} () {{}}", name);
 387                let filter_range = 3..3 + name.len();
 388                let display_range = 0..filter_range.end;
 389                (text, filter_range, display_range)
 390            }
 391            lsp::SymbolKind::STRUCT => {
 392                let text = format!("struct {} {{}}", name);
 393                let filter_range = 7..7 + name.len();
 394                let display_range = 0..filter_range.end;
 395                (text, filter_range, display_range)
 396            }
 397            lsp::SymbolKind::ENUM => {
 398                let text = format!("enum {} {{}}", name);
 399                let filter_range = 5..5 + name.len();
 400                let display_range = 0..filter_range.end;
 401                (text, filter_range, display_range)
 402            }
 403            lsp::SymbolKind::INTERFACE => {
 404                let text = format!("trait {} {{}}", name);
 405                let filter_range = 6..6 + name.len();
 406                let display_range = 0..filter_range.end;
 407                (text, filter_range, display_range)
 408            }
 409            lsp::SymbolKind::CONSTANT => {
 410                let text = format!("const {}: () = ();", name);
 411                let filter_range = 6..6 + name.len();
 412                let display_range = 0..filter_range.end;
 413                (text, filter_range, display_range)
 414            }
 415            lsp::SymbolKind::MODULE => {
 416                let text = format!("mod {} {{}}", name);
 417                let filter_range = 4..4 + name.len();
 418                let display_range = 0..filter_range.end;
 419                (text, filter_range, display_range)
 420            }
 421            lsp::SymbolKind::TYPE_PARAMETER => {
 422                let text = format!("type {} {{}}", name);
 423                let filter_range = 5..5 + name.len();
 424                let display_range = 0..filter_range.end;
 425                (text, filter_range, display_range)
 426            }
 427            _ => return None,
 428        };
 429
 430        Some(CodeLabel {
 431            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
 432            text: text[display_range].to_string(),
 433            filter_range,
 434        })
 435    }
 436}
 437
 438pub(crate) struct RustContextProvider;
 439
 440const RUST_PACKAGE_TASK_VARIABLE: VariableName =
 441    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
 442
 443/// The bin name corresponding to the current file in Cargo.toml
 444const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
 445    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
 446
 447const RUST_MAIN_FUNCTION_TASK_VARIABLE: VariableName =
 448    VariableName::Custom(Cow::Borrowed("_rust_main_function_end"));
 449
 450impl ContextProvider for RustContextProvider {
 451    fn build_context(
 452        &self,
 453        task_variables: &TaskVariables,
 454        location: &Location,
 455        project_env: Option<HashMap<String, String>>,
 456        _: Arc<dyn LanguageToolchainStore>,
 457        cx: &mut gpui::AppContext,
 458    ) -> Task<Result<TaskVariables>> {
 459        let local_abs_path = location
 460            .buffer
 461            .read(cx)
 462            .file()
 463            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
 464
 465        let local_abs_path = local_abs_path.as_deref();
 466
 467        let is_main_function = task_variables
 468            .get(&RUST_MAIN_FUNCTION_TASK_VARIABLE)
 469            .is_some();
 470
 471        if is_main_function {
 472            if let Some((package_name, bin_name)) = local_abs_path.and_then(|path| {
 473                package_name_and_bin_name_from_abs_path(path, project_env.as_ref())
 474            }) {
 475                return Task::ready(Ok(TaskVariables::from_iter([
 476                    (RUST_PACKAGE_TASK_VARIABLE.clone(), package_name),
 477                    (RUST_BIN_NAME_TASK_VARIABLE.clone(), bin_name),
 478                ])));
 479            }
 480        }
 481
 482        if let Some(package_name) = local_abs_path
 483            .and_then(|local_abs_path| local_abs_path.parent())
 484            .and_then(|path| human_readable_package_name(path, project_env.as_ref()))
 485        {
 486            return Task::ready(Ok(TaskVariables::from_iter([(
 487                RUST_PACKAGE_TASK_VARIABLE.clone(),
 488                package_name,
 489            )])));
 490        }
 491
 492        Task::ready(Ok(TaskVariables::default()))
 493    }
 494
 495    fn associated_tasks(
 496        &self,
 497        file: Option<Arc<dyn language::File>>,
 498        cx: &AppContext,
 499    ) -> Option<TaskTemplates> {
 500        const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
 501        let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx)
 502            .tasks
 503            .variables
 504            .get(DEFAULT_RUN_NAME_STR)
 505            .cloned();
 506        let run_task_args = if let Some(package_to_run) = package_to_run {
 507            vec!["run".into(), "-p".into(), package_to_run]
 508        } else {
 509            vec!["run".into()]
 510        };
 511        Some(TaskTemplates(vec![
 512            TaskTemplate {
 513                label: format!(
 514                    "cargo check -p {}",
 515                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 516                ),
 517                command: "cargo".into(),
 518                args: vec![
 519                    "check".into(),
 520                    "-p".into(),
 521                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 522                ],
 523                cwd: Some("$ZED_DIRNAME".to_owned()),
 524                ..TaskTemplate::default()
 525            },
 526            TaskTemplate {
 527                label: "cargo check --workspace --all-targets".into(),
 528                command: "cargo".into(),
 529                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
 530                cwd: Some("$ZED_DIRNAME".to_owned()),
 531                ..TaskTemplate::default()
 532            },
 533            TaskTemplate {
 534                label: format!(
 535                    "cargo test -p {} {} -- --nocapture",
 536                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 537                    VariableName::Symbol.template_value(),
 538                ),
 539                command: "cargo".into(),
 540                args: vec![
 541                    "test".into(),
 542                    "-p".into(),
 543                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 544                    VariableName::Symbol.template_value(),
 545                    "--".into(),
 546                    "--nocapture".into(),
 547                ],
 548                tags: vec!["rust-test".to_owned()],
 549                cwd: Some("$ZED_DIRNAME".to_owned()),
 550                ..TaskTemplate::default()
 551            },
 552            TaskTemplate {
 553                label: format!(
 554                    "cargo test -p {} {}",
 555                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 556                    VariableName::Stem.template_value(),
 557                ),
 558                command: "cargo".into(),
 559                args: vec![
 560                    "test".into(),
 561                    "-p".into(),
 562                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 563                    VariableName::Stem.template_value(),
 564                ],
 565                tags: vec!["rust-mod-test".to_owned()],
 566                cwd: Some("$ZED_DIRNAME".to_owned()),
 567                ..TaskTemplate::default()
 568            },
 569            TaskTemplate {
 570                label: format!(
 571                    "cargo run -p {} --bin {}",
 572                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 573                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 574                ),
 575                command: "cargo".into(),
 576                args: vec![
 577                    "run".into(),
 578                    "-p".into(),
 579                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 580                    "--bin".into(),
 581                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
 582                ],
 583                cwd: Some("$ZED_DIRNAME".to_owned()),
 584                tags: vec!["rust-main".to_owned()],
 585                ..TaskTemplate::default()
 586            },
 587            TaskTemplate {
 588                label: format!(
 589                    "cargo test -p {}",
 590                    RUST_PACKAGE_TASK_VARIABLE.template_value()
 591                ),
 592                command: "cargo".into(),
 593                args: vec![
 594                    "test".into(),
 595                    "-p".into(),
 596                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
 597                ],
 598                cwd: Some("$ZED_DIRNAME".to_owned()),
 599                ..TaskTemplate::default()
 600            },
 601            TaskTemplate {
 602                label: "cargo run".into(),
 603                command: "cargo".into(),
 604                args: run_task_args,
 605                cwd: Some("$ZED_DIRNAME".to_owned()),
 606                ..TaskTemplate::default()
 607            },
 608            TaskTemplate {
 609                label: "cargo clean".into(),
 610                command: "cargo".into(),
 611                args: vec!["clean".into()],
 612                cwd: Some("$ZED_DIRNAME".to_owned()),
 613                ..TaskTemplate::default()
 614            },
 615        ]))
 616    }
 617}
 618
 619/// Part of the data structure of Cargo metadata
 620#[derive(serde::Deserialize)]
 621struct CargoMetadata {
 622    packages: Vec<CargoPackage>,
 623}
 624
 625#[derive(serde::Deserialize)]
 626struct CargoPackage {
 627    id: String,
 628    targets: Vec<CargoTarget>,
 629}
 630
 631#[derive(serde::Deserialize)]
 632struct CargoTarget {
 633    name: String,
 634    kind: Vec<String>,
 635    src_path: String,
 636}
 637
 638fn package_name_and_bin_name_from_abs_path(
 639    abs_path: &Path,
 640    project_env: Option<&HashMap<String, String>>,
 641) -> Option<(String, String)> {
 642    let mut command = std::process::Command::new("cargo");
 643    if let Some(envs) = project_env {
 644        command.envs(envs);
 645    }
 646    let output = command
 647        .current_dir(abs_path.parent()?)
 648        .arg("metadata")
 649        .arg("--no-deps")
 650        .arg("--format-version")
 651        .arg("1")
 652        .output()
 653        .log_err()?
 654        .stdout;
 655
 656    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
 657
 658    retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
 659        |(package_id, bin_name)| {
 660            let package_name = package_name_from_pkgid(&package_id);
 661
 662            package_name.map(|package_name| (package_name.to_owned(), bin_name))
 663        },
 664    )
 665}
 666
 667fn retrieve_package_id_and_bin_name_from_metadata(
 668    metadata: CargoMetadata,
 669    abs_path: &Path,
 670) -> Option<(String, String)> {
 671    for package in metadata.packages {
 672        for target in package.targets {
 673            let is_bin = target.kind.iter().any(|kind| kind == "bin");
 674            let target_path = PathBuf::from(target.src_path);
 675            if target_path == abs_path && is_bin {
 676                return Some((package.id, target.name));
 677            }
 678        }
 679    }
 680
 681    None
 682}
 683
 684fn human_readable_package_name(
 685    package_directory: &Path,
 686    project_env: Option<&HashMap<String, String>>,
 687) -> Option<String> {
 688    let mut command = std::process::Command::new("cargo");
 689    if let Some(envs) = project_env {
 690        command.envs(envs);
 691    }
 692
 693    let pkgid = String::from_utf8(
 694        command
 695            .current_dir(package_directory)
 696            .arg("pkgid")
 697            .output()
 698            .log_err()?
 699            .stdout,
 700    )
 701    .ok()?;
 702    Some(package_name_from_pkgid(&pkgid)?.to_owned())
 703}
 704
 705// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
 706// Output example in the root of Zed project:
 707// ```sh
 708// ❯ cargo pkgid zed
 709// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
 710// ```
 711// Another variant, if a project has a custom package name or hyphen in the name:
 712// ```
 713// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
 714// ```
 715//
 716// Extracts the package name from the output according to the spec:
 717// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
 718fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
 719    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
 720        match input.rsplit_once(suffix_start) {
 721            Some((without_suffix, _)) => without_suffix,
 722            None => input,
 723        }
 724    }
 725
 726    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
 727    let package_name = match version_suffix.rsplit_once('@') {
 728        Some((custom_package_name, _version)) => custom_package_name,
 729        None => {
 730            let host_and_path = split_off_suffix(version_prefix, '?');
 731            let (_, package_name) = host_and_path.rsplit_once('/')?;
 732            package_name
 733        }
 734    };
 735    Some(package_name)
 736}
 737
 738async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
 739    maybe!(async {
 740        let mut last = None;
 741        let mut entries = fs::read_dir(&container_dir).await?;
 742        while let Some(entry) = entries.next().await {
 743            last = Some(entry?.path());
 744        }
 745
 746        anyhow::Ok(LanguageServerBinary {
 747            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
 748            env: None,
 749            arguments: Default::default(),
 750        })
 751    })
 752    .await
 753    .log_err()
 754}
 755
 756#[cfg(test)]
 757mod tests {
 758    use std::num::NonZeroU32;
 759
 760    use super::*;
 761    use crate::language;
 762    use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
 763    use language::language_settings::AllLanguageSettings;
 764    use lsp::CompletionItemLabelDetails;
 765    use settings::SettingsStore;
 766    use theme::SyntaxTheme;
 767
 768    #[gpui::test]
 769    async fn test_process_rust_diagnostics() {
 770        let mut params = lsp::PublishDiagnosticsParams {
 771            uri: lsp::Url::from_file_path("/a").unwrap(),
 772            version: None,
 773            diagnostics: vec![
 774                // no newlines
 775                lsp::Diagnostic {
 776                    message: "use of moved value `a`".to_string(),
 777                    ..Default::default()
 778                },
 779                // newline at the end of a code span
 780                lsp::Diagnostic {
 781                    message: "consider importing this struct: `use b::c;\n`".to_string(),
 782                    ..Default::default()
 783                },
 784                // code span starting right after a newline
 785                lsp::Diagnostic {
 786                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
 787                        .to_string(),
 788                    ..Default::default()
 789                },
 790            ],
 791        };
 792        RustLspAdapter.process_diagnostics(&mut params);
 793
 794        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
 795
 796        // remove trailing newline from code span
 797        assert_eq!(
 798            params.diagnostics[1].message,
 799            "consider importing this struct: `use b::c;`"
 800        );
 801
 802        // do not remove newline before the start of code span
 803        assert_eq!(
 804            params.diagnostics[2].message,
 805            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
 806        );
 807    }
 808
 809    #[gpui::test]
 810    async fn test_rust_label_for_completion() {
 811        let adapter = Arc::new(RustLspAdapter);
 812        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
 813        let grammar = language.grammar().unwrap();
 814        let theme = SyntaxTheme::new_test([
 815            ("type", Hsla::default()),
 816            ("keyword", Hsla::default()),
 817            ("function", Hsla::default()),
 818            ("property", Hsla::default()),
 819        ]);
 820
 821        language.set_theme(&theme);
 822
 823        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
 824        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
 825        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
 826        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
 827
 828        assert_eq!(
 829            adapter
 830                .label_for_completion(
 831                    &lsp::CompletionItem {
 832                        kind: Some(lsp::CompletionItemKind::FUNCTION),
 833                        label: "hello(…)".to_string(),
 834                        label_details: Some(CompletionItemLabelDetails {
 835                            detail: Some(" (use crate::foo)".into()),
 836                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
 837                        }),
 838                        ..Default::default()
 839                    },
 840                    &language
 841                )
 842                .await,
 843            Some(CodeLabel {
 844                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
 845                filter_range: 0..5,
 846                runs: vec![
 847                    (0..5, highlight_function),
 848                    (7..10, highlight_keyword),
 849                    (11..17, highlight_type),
 850                    (18..19, highlight_type),
 851                    (25..28, highlight_type),
 852                    (29..30, highlight_type),
 853                ],
 854            })
 855        );
 856        assert_eq!(
 857            adapter
 858                .label_for_completion(
 859                    &lsp::CompletionItem {
 860                        kind: Some(lsp::CompletionItemKind::FUNCTION),
 861                        label: "hello(…)".to_string(),
 862                        label_details: Some(CompletionItemLabelDetails {
 863                            detail: Some(" (use crate::foo)".into()),
 864                            description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
 865                        }),
 866                        ..Default::default()
 867                    },
 868                    &language
 869                )
 870                .await,
 871            Some(CodeLabel {
 872                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
 873                filter_range: 0..5,
 874                runs: vec![
 875                    (0..5, highlight_function),
 876                    (7..10, highlight_keyword),
 877                    (11..17, highlight_type),
 878                    (18..19, highlight_type),
 879                    (25..28, highlight_type),
 880                    (29..30, highlight_type),
 881                ],
 882            })
 883        );
 884        assert_eq!(
 885            adapter
 886                .label_for_completion(
 887                    &lsp::CompletionItem {
 888                        kind: Some(lsp::CompletionItemKind::FIELD),
 889                        label: "len".to_string(),
 890                        detail: Some("usize".to_string()),
 891                        ..Default::default()
 892                    },
 893                    &language
 894                )
 895                .await,
 896            Some(CodeLabel {
 897                text: "len: usize".to_string(),
 898                filter_range: 0..3,
 899                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
 900            })
 901        );
 902
 903        assert_eq!(
 904            adapter
 905                .label_for_completion(
 906                    &lsp::CompletionItem {
 907                        kind: Some(lsp::CompletionItemKind::FUNCTION),
 908                        label: "hello(…)".to_string(),
 909                        label_details: Some(CompletionItemLabelDetails {
 910                            detail: Some(" (use crate::foo)".to_string()),
 911                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
 912                        }),
 913
 914                        ..Default::default()
 915                    },
 916                    &language
 917                )
 918                .await,
 919            Some(CodeLabel {
 920                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
 921                filter_range: 0..5,
 922                runs: vec![
 923                    (0..5, highlight_function),
 924                    (7..10, highlight_keyword),
 925                    (11..17, highlight_type),
 926                    (18..19, highlight_type),
 927                    (25..28, highlight_type),
 928                    (29..30, highlight_type),
 929                ],
 930            })
 931        );
 932    }
 933
 934    #[gpui::test]
 935    async fn test_rust_label_for_symbol() {
 936        let adapter = Arc::new(RustLspAdapter);
 937        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
 938        let grammar = language.grammar().unwrap();
 939        let theme = SyntaxTheme::new_test([
 940            ("type", Hsla::default()),
 941            ("keyword", Hsla::default()),
 942            ("function", Hsla::default()),
 943            ("property", Hsla::default()),
 944        ]);
 945
 946        language.set_theme(&theme);
 947
 948        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
 949        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
 950        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
 951
 952        assert_eq!(
 953            adapter
 954                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
 955                .await,
 956            Some(CodeLabel {
 957                text: "fn hello".to_string(),
 958                filter_range: 3..8,
 959                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
 960            })
 961        );
 962
 963        assert_eq!(
 964            adapter
 965                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
 966                .await,
 967            Some(CodeLabel {
 968                text: "type World".to_string(),
 969                filter_range: 5..10,
 970                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
 971            })
 972        );
 973    }
 974
 975    #[gpui::test]
 976    async fn test_rust_autoindent(cx: &mut TestAppContext) {
 977        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
 978        cx.update(|cx| {
 979            let test_settings = SettingsStore::test(cx);
 980            cx.set_global(test_settings);
 981            language::init(cx);
 982            cx.update_global::<SettingsStore, _>(|store, cx| {
 983                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
 984                    s.defaults.tab_size = NonZeroU32::new(2);
 985                });
 986            });
 987        });
 988
 989        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
 990
 991        cx.new_model(|cx| {
 992            let mut buffer = Buffer::local("", cx).with_language(language, cx);
 993
 994            // indent between braces
 995            buffer.set_text("fn a() {}", cx);
 996            let ix = buffer.len() - 1;
 997            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
 998            assert_eq!(buffer.text(), "fn a() {\n  \n}");
 999
1000            // indent between braces, even after empty lines
1001            buffer.set_text("fn a() {\n\n\n}", cx);
1002            let ix = buffer.len() - 2;
1003            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1004            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1005
1006            // indent a line that continues a field expression
1007            buffer.set_text("fn a() {\n  \n}", cx);
1008            let ix = buffer.len() - 2;
1009            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1010            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1011
1012            // indent further lines that continue the field expression, even after empty lines
1013            let ix = buffer.len() - 2;
1014            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1015            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1016
1017            // dedent the line after the field expression
1018            let ix = buffer.len() - 2;
1019            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1020            assert_eq!(
1021                buffer.text(),
1022                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1023            );
1024
1025            // indent inside a struct within a call
1026            buffer.set_text("const a: B = c(D {});", cx);
1027            let ix = buffer.len() - 3;
1028            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1029            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1030
1031            // indent further inside a nested call
1032            let ix = buffer.len() - 4;
1033            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1034            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1035
1036            // keep that indent after an empty line
1037            let ix = buffer.len() - 8;
1038            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1039            assert_eq!(
1040                buffer.text(),
1041                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1042            );
1043
1044            buffer
1045        });
1046    }
1047
1048    #[test]
1049    fn test_package_name_from_pkgid() {
1050        for (input, expected) in [
1051            (
1052                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1053                "zed",
1054            ),
1055            (
1056                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1057                "my-custom-package",
1058            ),
1059        ] {
1060            assert_eq!(package_name_from_pkgid(input), Some(expected));
1061        }
1062    }
1063
1064    #[test]
1065    fn test_retrieve_package_id_and_bin_name_from_metadata() {
1066        for (input, absolute_path, expected) in [
1067            (
1068                r#"{"packages":[{"id":"path+file:///path/to/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
1069                "/path/to/zed/src/main.rs",
1070                Some(("path+file:///path/to/zed/crates/zed#0.131.0", "zed")),
1071            ),
1072            (
1073                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"}]}]}"#,
1074                "/path/to/custom-package/src/main.rs",
1075                Some((
1076                    "path+file:///path/to/custom-package#my-custom-package@0.1.0",
1077                    "my-custom-bin",
1078                )),
1079            ),
1080            (
1081                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"}]}]}"#,
1082                "/path/to/custom-package/src/main.rs",
1083                None,
1084            ),
1085        ] {
1086            let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
1087
1088            let absolute_path = Path::new(absolute_path);
1089
1090            assert_eq!(
1091                retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
1092                expected.map(|(pkgid, bin)| (pkgid.to_owned(), bin.to_owned()))
1093            );
1094        }
1095    }
1096}