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