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