rust.rs

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