rust.rs

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