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