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, LazyLock},
  18};
  19use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
  20use util::{fs::remove_matching, maybe, ResultExt};
  21
  22use crate::language_settings::language_settings;
  23
  24pub struct RustLspAdapter;
  25
  26#[cfg(target_os = "macos")]
  27impl RustLspAdapter {
  28    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  29    const ARCH_SERVER_NAME: &str = "apple-darwin";
  30}
  31
  32#[cfg(target_os = "linux")]
  33impl RustLspAdapter {
  34    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  35    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
  36}
  37
  38#[cfg(target_os = "freebsd")]
  39impl RustLspAdapter {
  40    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
  41    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
  42}
  43
  44#[cfg(target_os = "windows")]
  45impl RustLspAdapter {
  46    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
  47    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
  48}
  49
  50impl RustLspAdapter {
  51    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
  52
  53    fn build_asset_name() -> String {
  54        let extension = match Self::GITHUB_ASSET_KIND {
  55            AssetKind::TarGz => "tar.gz",
  56            AssetKind::Gz => "gz",
  57            AssetKind::Zip => "zip",
  58        };
  59
  60        format!(
  61            "{}-{}-{}.{}",
  62            Self::SERVER_NAME,
  63            std::env::consts::ARCH,
  64            Self::ARCH_SERVER_NAME,
  65            extension
  66        )
  67    }
  68}
  69
  70#[async_trait(?Send)]
  71impl LspAdapter for RustLspAdapter {
  72    fn name(&self) -> LanguageServerName {
  73        Self::SERVER_NAME.clone()
  74    }
  75
  76    async fn check_if_user_installed(
  77        &self,
  78        delegate: &dyn LspAdapterDelegate,
  79        _: Arc<dyn LanguageToolchainStore>,
  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 = util::command::new_std_command("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 = util::command::new_std_command("cargo");
 689    if let Some(envs) = project_env {
 690        command.envs(envs);
 691    }
 692    let pkgid = String::from_utf8(
 693        command
 694            .current_dir(package_directory)
 695            .arg("pkgid")
 696            .output()
 697            .log_err()?
 698            .stdout,
 699    )
 700    .ok()?;
 701    Some(package_name_from_pkgid(&pkgid)?.to_owned())
 702}
 703
 704// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
 705// Output example in the root of Zed project:
 706// ```sh
 707// ❯ cargo pkgid zed
 708// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
 709// ```
 710// Another variant, if a project has a custom package name or hyphen in the name:
 711// ```
 712// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
 713// ```
 714//
 715// Extracts the package name from the output according to the spec:
 716// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
 717fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
 718    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
 719        match input.rsplit_once(suffix_start) {
 720            Some((without_suffix, _)) => without_suffix,
 721            None => input,
 722        }
 723    }
 724
 725    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
 726    let package_name = match version_suffix.rsplit_once('@') {
 727        Some((custom_package_name, _version)) => custom_package_name,
 728        None => {
 729            let host_and_path = split_off_suffix(version_prefix, '?');
 730            let (_, package_name) = host_and_path.rsplit_once('/')?;
 731            package_name
 732        }
 733    };
 734    Some(package_name)
 735}
 736
 737async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
 738    maybe!(async {
 739        let mut last = None;
 740        let mut entries = fs::read_dir(&container_dir).await?;
 741        while let Some(entry) = entries.next().await {
 742            last = Some(entry?.path());
 743        }
 744
 745        anyhow::Ok(LanguageServerBinary {
 746            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
 747            env: None,
 748            arguments: Default::default(),
 749        })
 750    })
 751    .await
 752    .log_err()
 753}
 754
 755#[cfg(test)]
 756mod tests {
 757    use std::num::NonZeroU32;
 758
 759    use super::*;
 760    use crate::language;
 761    use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
 762    use language::language_settings::AllLanguageSettings;
 763    use lsp::CompletionItemLabelDetails;
 764    use settings::SettingsStore;
 765    use theme::SyntaxTheme;
 766
 767    #[gpui::test]
 768    async fn test_process_rust_diagnostics() {
 769        let mut params = lsp::PublishDiagnosticsParams {
 770            uri: lsp::Url::from_file_path("/a").unwrap(),
 771            version: None,
 772            diagnostics: vec![
 773                // no newlines
 774                lsp::Diagnostic {
 775                    message: "use of moved value `a`".to_string(),
 776                    ..Default::default()
 777                },
 778                // newline at the end of a code span
 779                lsp::Diagnostic {
 780                    message: "consider importing this struct: `use b::c;\n`".to_string(),
 781                    ..Default::default()
 782                },
 783                // code span starting right after a newline
 784                lsp::Diagnostic {
 785                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
 786                        .to_string(),
 787                    ..Default::default()
 788                },
 789            ],
 790        };
 791        RustLspAdapter.process_diagnostics(&mut params);
 792
 793        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
 794
 795        // remove trailing newline from code span
 796        assert_eq!(
 797            params.diagnostics[1].message,
 798            "consider importing this struct: `use b::c;`"
 799        );
 800
 801        // do not remove newline before the start of code span
 802        assert_eq!(
 803            params.diagnostics[2].message,
 804            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
 805        );
 806    }
 807
 808    #[gpui::test]
 809    async fn test_rust_label_for_completion() {
 810        let adapter = Arc::new(RustLspAdapter);
 811        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
 812        let grammar = language.grammar().unwrap();
 813        let theme = SyntaxTheme::new_test([
 814            ("type", Hsla::default()),
 815            ("keyword", Hsla::default()),
 816            ("function", Hsla::default()),
 817            ("property", Hsla::default()),
 818        ]);
 819
 820        language.set_theme(&theme);
 821
 822        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
 823        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
 824        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
 825        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
 826
 827        assert_eq!(
 828            adapter
 829                .label_for_completion(
 830                    &lsp::CompletionItem {
 831                        kind: Some(lsp::CompletionItemKind::FUNCTION),
 832                        label: "hello(…)".to_string(),
 833                        label_details: Some(CompletionItemLabelDetails {
 834                            detail: Some(" (use crate::foo)".into()),
 835                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string())
 836                        }),
 837                        ..Default::default()
 838                    },
 839                    &language
 840                )
 841                .await,
 842            Some(CodeLabel {
 843                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
 844                filter_range: 0..5,
 845                runs: vec![
 846                    (0..5, highlight_function),
 847                    (7..10, highlight_keyword),
 848                    (11..17, highlight_type),
 849                    (18..19, highlight_type),
 850                    (25..28, highlight_type),
 851                    (29..30, highlight_type),
 852                ],
 853            })
 854        );
 855        assert_eq!(
 856            adapter
 857                .label_for_completion(
 858                    &lsp::CompletionItem {
 859                        kind: Some(lsp::CompletionItemKind::FUNCTION),
 860                        label: "hello(…)".to_string(),
 861                        label_details: Some(CompletionItemLabelDetails {
 862                            detail: Some(" (use crate::foo)".into()),
 863                            description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
 864                        }),
 865                        ..Default::default()
 866                    },
 867                    &language
 868                )
 869                .await,
 870            Some(CodeLabel {
 871                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
 872                filter_range: 0..5,
 873                runs: vec![
 874                    (0..5, highlight_function),
 875                    (7..10, highlight_keyword),
 876                    (11..17, highlight_type),
 877                    (18..19, highlight_type),
 878                    (25..28, highlight_type),
 879                    (29..30, highlight_type),
 880                ],
 881            })
 882        );
 883        assert_eq!(
 884            adapter
 885                .label_for_completion(
 886                    &lsp::CompletionItem {
 887                        kind: Some(lsp::CompletionItemKind::FIELD),
 888                        label: "len".to_string(),
 889                        detail: Some("usize".to_string()),
 890                        ..Default::default()
 891                    },
 892                    &language
 893                )
 894                .await,
 895            Some(CodeLabel {
 896                text: "len: usize".to_string(),
 897                filter_range: 0..3,
 898                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
 899            })
 900        );
 901
 902        assert_eq!(
 903            adapter
 904                .label_for_completion(
 905                    &lsp::CompletionItem {
 906                        kind: Some(lsp::CompletionItemKind::FUNCTION),
 907                        label: "hello(…)".to_string(),
 908                        label_details: Some(CompletionItemLabelDetails {
 909                            detail: Some(" (use crate::foo)".to_string()),
 910                            description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
 911                        }),
 912
 913                        ..Default::default()
 914                    },
 915                    &language
 916                )
 917                .await,
 918            Some(CodeLabel {
 919                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
 920                filter_range: 0..5,
 921                runs: vec![
 922                    (0..5, highlight_function),
 923                    (7..10, highlight_keyword),
 924                    (11..17, highlight_type),
 925                    (18..19, highlight_type),
 926                    (25..28, highlight_type),
 927                    (29..30, highlight_type),
 928                ],
 929            })
 930        );
 931    }
 932
 933    #[gpui::test]
 934    async fn test_rust_label_for_symbol() {
 935        let adapter = Arc::new(RustLspAdapter);
 936        let language = language("rust", tree_sitter_rust::LANGUAGE.into());
 937        let grammar = language.grammar().unwrap();
 938        let theme = SyntaxTheme::new_test([
 939            ("type", Hsla::default()),
 940            ("keyword", Hsla::default()),
 941            ("function", Hsla::default()),
 942            ("property", Hsla::default()),
 943        ]);
 944
 945        language.set_theme(&theme);
 946
 947        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
 948        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
 949        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
 950
 951        assert_eq!(
 952            adapter
 953                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
 954                .await,
 955            Some(CodeLabel {
 956                text: "fn hello".to_string(),
 957                filter_range: 3..8,
 958                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
 959            })
 960        );
 961
 962        assert_eq!(
 963            adapter
 964                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
 965                .await,
 966            Some(CodeLabel {
 967                text: "type World".to_string(),
 968                filter_range: 5..10,
 969                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
 970            })
 971        );
 972    }
 973
 974    #[gpui::test]
 975    async fn test_rust_autoindent(cx: &mut TestAppContext) {
 976        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
 977        cx.update(|cx| {
 978            let test_settings = SettingsStore::test(cx);
 979            cx.set_global(test_settings);
 980            language::init(cx);
 981            cx.update_global::<SettingsStore, _>(|store, cx| {
 982                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
 983                    s.defaults.tab_size = NonZeroU32::new(2);
 984                });
 985            });
 986        });
 987
 988        let language = crate::language("rust", tree_sitter_rust::LANGUAGE.into());
 989
 990        cx.new_model(|cx| {
 991            let mut buffer = Buffer::local("", cx).with_language(language, cx);
 992
 993            // indent between braces
 994            buffer.set_text("fn a() {}", cx);
 995            let ix = buffer.len() - 1;
 996            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
 997            assert_eq!(buffer.text(), "fn a() {\n  \n}");
 998
 999            // indent between braces, even after empty lines
1000            buffer.set_text("fn a() {\n\n\n}", cx);
1001            let ix = buffer.len() - 2;
1002            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1003            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
1004
1005            // indent a line that continues a field expression
1006            buffer.set_text("fn a() {\n  \n}", cx);
1007            let ix = buffer.len() - 2;
1008            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
1009            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
1010
1011            // indent further lines that continue the field expression, even after empty lines
1012            let ix = buffer.len() - 2;
1013            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
1014            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
1015
1016            // dedent the line after the field expression
1017            let ix = buffer.len() - 2;
1018            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
1019            assert_eq!(
1020                buffer.text(),
1021                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
1022            );
1023
1024            // indent inside a struct within a call
1025            buffer.set_text("const a: B = c(D {});", cx);
1026            let ix = buffer.len() - 3;
1027            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
1028            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
1029
1030            // indent further inside a nested call
1031            let ix = buffer.len() - 4;
1032            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
1033            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
1034
1035            // keep that indent after an empty line
1036            let ix = buffer.len() - 8;
1037            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
1038            assert_eq!(
1039                buffer.text(),
1040                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
1041            );
1042
1043            buffer
1044        });
1045    }
1046
1047    #[test]
1048    fn test_package_name_from_pkgid() {
1049        for (input, expected) in [
1050            (
1051                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
1052                "zed",
1053            ),
1054            (
1055                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
1056                "my-custom-package",
1057            ),
1058        ] {
1059            assert_eq!(package_name_from_pkgid(input), Some(expected));
1060        }
1061    }
1062
1063    #[test]
1064    fn test_retrieve_package_id_and_bin_name_from_metadata() {
1065        for (input, absolute_path, expected) in [
1066            (
1067                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"}]}]}"#,
1068                "/path/to/zed/src/main.rs",
1069                Some(("path+file:///path/to/zed/crates/zed#0.131.0", "zed")),
1070            ),
1071            (
1072                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"}]}]}"#,
1073                "/path/to/custom-package/src/main.rs",
1074                Some((
1075                    "path+file:///path/to/custom-package#my-custom-package@0.1.0",
1076                    "my-custom-bin",
1077                )),
1078            ),
1079            (
1080                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"}]}]}"#,
1081                "/path/to/custom-package/src/main.rs",
1082                None,
1083            ),
1084        ] {
1085            let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
1086
1087            let absolute_path = Path::new(absolute_path);
1088
1089            assert_eq!(
1090                retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
1091                expected.map(|(pkgid, bin)| (pkgid.to_owned(), bin.to_owned()))
1092            );
1093        }
1094    }
1095}