2393 lines · 89.8 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use async_trait::async_trait;use collections::HashMap;use futures::StreamExt;use futures::lock::OwnedMutexGuard;use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task};use http_client::github::AssetKind;use http_client::github::{GitHubLspBinaryVersion, latest_github_release};use http_client::github_download::{GithubBinaryMetadata, download_server_binary};pub use language::*;use lsp::{InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions};use project::lsp_store::lsp_ext_command;use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;use project::project_settings::ProjectSettings;use regex::Regex;use serde_json::json;use settings::{SemanticTokenRules, Settings as _};use smallvec::SmallVec;use smol::fs::{self};use std::cmp::Reverse;use std::fmt::Display;use std::future::Future;use std::ops::Range;use std::{    borrow::Cow,    path::{Path, PathBuf},    sync::{Arc, LazyLock},};use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};use util::command::{Stdio, new_command};use util::fs::{make_file_executable, remove_matching};use util::merge_json_value_into;use util::rel_path::RelPath;use util::{ResultExt, maybe}; use crate::language_settings::LanguageSettings; pub(crate) fn semantic_token_rules() -> SemanticTokenRules {    let content = grammars::get_file("rust/semantic_token_rules.json")        .expect("missing rust/semantic_token_rules.json");    let json = std::str::from_utf8(&content.data).expect("invalid utf-8 in semantic_token_rules");    settings::parse_json_with_comments::<SemanticTokenRules>(json)        .expect("failed to parse rust semantic_token_rules.json")} pub struct RustLspAdapter; #[cfg(target_os = "macos")]impl RustLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;    const ARCH_SERVER_NAME: &str = "apple-darwin";} #[cfg(target_os = "linux")]impl RustLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;    const ARCH_SERVER_NAME: &str = "unknown-linux";} #[cfg(target_os = "freebsd")]impl RustLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;    const ARCH_SERVER_NAME: &str = "unknown-freebsd";} #[cfg(target_os = "windows")]impl RustLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";} const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer"); #[cfg(target_os = "linux")]enum LibcType {    Gnu,    Musl,} impl RustLspAdapter {    fn convert_rust_analyzer_schema(raw_schema: &serde_json::Value) -> serde_json::Value {        let Some(schema_array) = raw_schema.as_array() else {            return raw_schema.clone();        };         let mut root_properties = serde_json::Map::new();         for item in schema_array {            if let Some(props) = item.get("properties").and_then(|p| p.as_object()) {                for (key, value) in props {                    let parts: Vec<&str> = key.split('.').collect();                     if parts.is_empty() {                        continue;                    }                     let parts_to_process = if parts.first() == Some(&"rust-analyzer") {                        &parts[1..]                    } else {                        &parts[..]                    };                     if parts_to_process.is_empty() {                        continue;                    }                     let mut current = &mut root_properties;                     for (i, part) in parts_to_process.iter().enumerate() {                        let is_last = i == parts_to_process.len() - 1;                         if is_last {                            current.insert(part.to_string(), value.clone());                        } else {                            let next_current = current                                .entry(part.to_string())                                .or_insert_with(|| {                                    serde_json::json!({                                        "type": "object",                                        "properties": {}                                    })                                })                                .as_object_mut()                                .expect("should be an object")                                .entry("properties")                                .or_insert_with(|| serde_json::json!({}))                                .as_object_mut()                                .expect("properties should be an object");                             current = next_current;                        }                    }                }            }        }         serde_json::json!({            "type": "object",            "properties": root_properties        })    }     #[cfg(target_os = "linux")]    async fn determine_libc_type() -> LibcType {        use futures::pin_mut;         async fn from_ldd_version() -> Option<LibcType> {            use util::command::new_command;             let ldd_output = new_command("ldd").arg("--version").output().await.ok()?;            let ldd_version = String::from_utf8_lossy(&ldd_output.stdout);             if ldd_version.contains("GNU libc") || ldd_version.contains("GLIBC") {                Some(LibcType::Gnu)            } else if ldd_version.contains("musl") {                Some(LibcType::Musl)            } else {                None            }        }         if let Some(libc_type) = from_ldd_version().await {            return libc_type;        }         let Ok(dir_entries) = smol::fs::read_dir("/lib").await else {            // defaulting to gnu because nix doesn't have /lib files due to not following FHS            return LibcType::Gnu;        };        let dir_entries = dir_entries.filter_map(async move |e| e.ok());        pin_mut!(dir_entries);         let mut has_musl = false;        let mut has_gnu = false;         while let Some(entry) = dir_entries.next().await {            let file_name = entry.file_name();            let file_name = file_name.to_string_lossy();            if file_name.starts_with("ld-musl-") {                has_musl = true;            } else if file_name.starts_with("ld-linux-") {                has_gnu = true;            }        }         match (has_musl, has_gnu) {            (true, _) => LibcType::Musl,            (_, true) => LibcType::Gnu,            _ => LibcType::Gnu,        }    }     #[cfg(target_os = "linux")]    async fn build_arch_server_name_linux() -> String {        let libc = match Self::determine_libc_type().await {            LibcType::Musl => "musl",            LibcType::Gnu => "gnu",        };         format!("{}-{}", Self::ARCH_SERVER_NAME, libc)    }     async fn rustup_rust_analyzer_for_worktree(        delegate: &dyn LspAdapterDelegate,    ) -> Option<PathBuf> {        if !Self::workspace_has_rust_toolchain_override(delegate).await {            return None;        }         let rustup = delegate.which("rustup".as_ref()).await?;        let env = delegate.shell_env().await;        let worktree_root = delegate.worktree_root_path();        let output = new_command(rustup)            .args(["which", "rust-analyzer"])            .envs(env.iter())            .current_dir(worktree_root)            .stdout(Stdio::piped())            .stderr(Stdio::piped())            .output()            .await;        let output = match output {            Ok(output) if output.status.success() => output,            Ok(output) => {                log::debug!(                    "failed to locate rust-analyzer through rustup in {worktree_root:?}: {}",                    String::from_utf8_lossy(&output.stderr)                );                return None;            }            Err(err) => {                log::debug!(                    "failed to run `rustup which rust-analyzer` in {worktree_root:?}: {err:#}"                );                return None;            }        };         let path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());        Some(path).filter(|p| !p.as_os_str().is_empty())    }     async fn workspace_has_rust_toolchain_override(delegate: &dyn LspAdapterDelegate) -> bool {        for file_name in ["rust-toolchain.toml", "rust-toolchain"] {            if fs::metadata(delegate.resolve_relative_path(PathBuf::from(file_name)))                .await                .is_ok()            {                return true;            }        }         false    }     async fn build_asset_name() -> String {        let extension = match Self::GITHUB_ASSET_KIND {            AssetKind::TarGz => "tar.gz",            AssetKind::TarBz2 => "tar.bz2",            AssetKind::Gz => "gz",            AssetKind::Zip => "zip",        };         #[cfg(target_os = "linux")]        let arch_server_name = Self::build_arch_server_name_linux().await;        #[cfg(not(target_os = "linux"))]        let arch_server_name = Self::ARCH_SERVER_NAME.to_string();         format!(            "{}-{}-{}.{}",            SERVER_NAME,            std::env::consts::ARCH,            &arch_server_name,            extension        )    }} pub(crate) struct CargoManifestProvider; impl ManifestProvider for CargoManifestProvider {    fn name(&self) -> ManifestName {        SharedString::new_static("Cargo.toml").into()    }     fn search(        &self,        ManifestQuery {            path,            depth,            delegate,        }: ManifestQuery,    ) -> Option<Arc<RelPath>> {        let mut outermost_cargo_toml = None;        for path in path.ancestors().take(depth) {            let p = path.join(RelPath::from_unix_str("Cargo.toml").unwrap());            if delegate.exists(&p, Some(false)) {                outermost_cargo_toml = Some(Arc::from(path));            }        }         outermost_cargo_toml    }} #[async_trait(?Send)]impl LspAdapter for RustLspAdapter {    fn name(&self) -> LanguageServerName {        SERVER_NAME    }     fn disk_based_diagnostic_sources(&self) -> Vec<String> {        vec![CARGO_DIAGNOSTICS_SOURCE_NAME.to_owned()]    }     fn disk_based_diagnostics_progress_token(&self) -> Option<String> {        Some("rust-analyzer/flycheck".into())    }     fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams, _: LanguageServerId) {        static REGEX: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));         for diagnostic in &mut params.diagnostics {            for message in diagnostic                .related_information                .iter_mut()                .flatten()                .map(|info| &mut info.message)                .chain([&mut diagnostic.message])            {                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {                    *message = sanitized;                }            }        }    }     fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {        static REGEX: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"(?m)\n *").expect("Failed to create REGEX"));        Some(REGEX.replace_all(message, "\n\n").to_string())    }     async fn label_for_completion(        &self,        completion: &lsp::CompletionItem,        language: &Arc<Language>,    ) -> Option<CodeLabel> {        // rust-analyzer calls these detail left and detail right in terms of where it expects things to be rendered        // this usually contains signatures of the thing to be completed        let detail_right = completion            .label_details            .as_ref()            .and_then(|detail| detail.description.as_ref())            .or(completion.detail.as_ref())            .map(|detail| detail.trim());        // this tends to contain alias and import information        let mut detail_left = completion            .label_details            .as_ref()            .and_then(|detail| detail.detail.as_deref());        let mk_label = |text: String, filter_range: &dyn Fn() -> Range<usize>, runs| {            let filter_range = completion                .filter_text                .as_deref()                .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))                .or_else(|| {                    text.find(&completion.label)                        .map(|ix| ix..ix + completion.label.len())                })                .unwrap_or_else(filter_range);             CodeLabel::new(text, filter_range, runs)        };        let mut label = match (detail_right, completion.kind) {            (Some(signature), Some(lsp::CompletionItemKind::FIELD)) => {                let name = &completion.label;                let text = format!("{name}: {signature}");                let prefix = "struct S { ";                let source = Rope::from_iter([prefix, &text, " }"]);                let runs =                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());                mk_label(text, &|| 0..completion.label.len(), runs)            }            (                Some(signature),                Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),            ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {                let name = &completion.label;                let text = format!("{name}: {signature}",);                let prefix = "let ";                let source = Rope::from_iter([prefix, &text, " = ();"]);                let runs =                    language.highlight_text(&source, prefix.len()..prefix.len() + text.len());                mk_label(text, &|| 0..completion.label.len(), runs)            }            (                function_signature,                Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),            ) => {                const FUNCTION_PREFIXES: [&str; 6] = [                    "async fn",                    "async unsafe fn",                    "const fn",                    "const unsafe fn",                    "unsafe fn",                    "fn",                ];                let fn_prefixed = FUNCTION_PREFIXES.iter().find_map(|&prefix| {                    function_signature?                        .strip_prefix(prefix)                        .map(|suffix| (prefix, suffix))                });                let label = if let Some(label) = completion                    .label                    .strip_suffix("(…)")                    .or_else(|| completion.label.strip_suffix("()"))                {                    label                } else {                    &completion.label                };                 static FULL_SIGNATURE_REGEX: LazyLock<Regex> =                    LazyLock::new(|| Regex::new(r"fn (.+?)\(").expect("Failed to create REGEX"));                if let Some((function_signature, match_)) = function_signature                    .filter(|it| it.contains(&label))                    .and_then(|it| Some((it, FULL_SIGNATURE_REGEX.find(it)?)))                {                    let source = Rope::from(function_signature);                    let runs = language.highlight_text(&source, 0..function_signature.len());                    mk_label(                        function_signature.to_owned(),                        &|| match_.range().start + 3..match_.range().end - 1,                        runs,                    )                } else if let Some((prefix, suffix)) = fn_prefixed {                    let text = format!("{label}{suffix}");                    let source = Rope::from_iter([prefix, " ", &text, " {}"]);                    let run_start = prefix.len() + 1;                    let runs = language.highlight_text(&source, run_start..run_start + text.len());                    mk_label(text, &|| 0..label.len(), runs)                } else if completion                    .detail                    .as_ref()                    .is_some_and(|detail| detail.starts_with("macro_rules! "))                {                    let text = completion.label.clone();                    let len = text.len();                    let source = Rope::from(text.as_str());                    let runs = language.highlight_text(&source, 0..len);                    mk_label(text, &|| 0..completion.label.len(), runs)                } else if detail_left.is_none() {                    return None;                } else {                    mk_label(                        completion.label.clone(),                        &|| 0..completion.label.len(),                        vec![],                    )                }            }            (_, kind) => {                let mut label;                let mut runs = vec![];                 if completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)                    && let Some(                        lsp::CompletionTextEdit::InsertAndReplace(lsp::InsertReplaceEdit {                            new_text,                            ..                        })                        | lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),                    ) = completion.text_edit.as_ref()                    && let Ok(mut snippet) = snippet::Snippet::parse(new_text)                    && snippet.tabstops.len() > 1                {                    label = String::new();                     // we never display the final tabstop                    snippet.tabstops.remove(snippet.tabstops.len() - 1);                     let mut text_pos = 0;                     let mut all_stop_ranges = snippet                        .tabstops                        .into_iter()                        .flat_map(|stop| stop.ranges)                        .collect::<SmallVec<[_; 8]>>();                    all_stop_ranges.sort_unstable_by_key(|a| (a.start, Reverse(a.end)));                     // Placeholders may nest, e.g. `$2` inside `${1:"$2"}`                    struct OpenPlaceholder {                        snippet_text_end: usize,                        label_run_start: usize,                    }                    let mut open_placeholders = SmallVec::<[OpenPlaceholder; 4]>::new();                     for range in &all_stop_ranges {                        let start_pos = range.start as usize;

Lines 1–500 of 2393.