diff --git a/Cargo.lock b/Cargo.lock index 1523063409f6061ab19dad863f5124c95f177627..0351fb336e1e00c66921716dee5055e7ada58d2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9331,6 +9331,7 @@ dependencies = [ "tree-sitter-typescript", "tree-sitter-yaml", "unindent", + "url", "util", "workspace", "workspace-hack", diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 9390c785f10e788a6ddea9efddca70f2b8514908..44b0cb58653adff27e31c6edd7fb4208e0bb4bbf 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -69,6 +69,7 @@ serde_json_lenient.workspace = true settings.workspace = true smol.workspace = true snippet_provider.workspace = true +url.workspace = true task.workspace = true tempfile.workspace = true toml.workspace = true diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index c89e462d59ab56bedd7cab9af0e7dc8730eee23b..f71234a7d5c5219b679acdd12610da4fe6e857ad 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -91,8 +91,9 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime let json_lsp_adapter = Arc::new(json::JsonLspAdapter::new(node.clone(), languages.clone())); let node_version_lsp_adapter = Arc::new(json::NodeVersionAdapter); let py_lsp_adapter = Arc::new(python::PyLspAdapter::new()); + let ty_lsp_adapter = Arc::new(python::TyLspAdapter::new(fs.clone())); let python_context_provider = Arc::new(python::PythonContextProvider); - let python_lsp_adapter = Arc::new(python::PythonLspAdapter::new(node.clone())); + let python_lsp_adapter = Arc::new(python::PyrightLspAdapter::new(node.clone())); let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new()); let ruff_lsp_adapter = Arc::new(RuffLspAdapter::new(fs.clone())); let python_toolchain_provider = Arc::new(python::PythonToolchainProvider); @@ -268,8 +269,10 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime LanguageServerName("typescript-language-server".into()), typescript_lsp_adapter, ); + languages.register_available_lsp_adapter(python_lsp_adapter.name(), python_lsp_adapter); languages.register_available_lsp_adapter(py_lsp_adapter.name(), py_lsp_adapter); + languages.register_available_lsp_adapter(ty_lsp_adapter.name(), ty_lsp_adapter); // Register Tailwind for the existing languages that should have it by default. // // This can be driven by the `language_servers` setting once we have a way for diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index c5abf07069752b834a4bd4e60735247eb2a38500..71d847d9f2fc44aeb52611b92b774b49dc615293 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -2,19 +2,14 @@ use anyhow::{Context as _, ensure}; use anyhow::{Result, anyhow}; use async_trait::async_trait; use collections::HashMap; -use dap::adapters::latest_github_release; use futures::{AsyncBufReadExt, StreamExt as _}; -use gpui::{App, Task}; -use gpui::{AsyncApp, SharedString}; -use http_client::github::AssetKind; -use http_client::github::GitHubLspBinaryVersion; -use language::ToolchainList; -use language::ToolchainLister; +use gpui::{App, AsyncApp, SharedString, Task}; +use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release}; use language::language_settings::language_settings; use language::{ContextLocation, LanguageToolchainStore}; use language::{ContextProvider, LspAdapter, LspAdapterDelegate}; use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; -use language::{Toolchain, ToolchainMetadata}; +use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata}; use lsp::LanguageServerBinary; use lsp::LanguageServerName; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -106,20 +101,243 @@ fn process_pyright_completions(items: &mut [lsp::CompletionItem]) { item.sort_text.take(); } } -pub struct PythonLspAdapter { + +pub struct TyLspAdapter { + fs: Arc, +} + +#[cfg(target_os = "macos")] +impl TyLspAdapter { + const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz; + const ARCH_SERVER_NAME: &str = "apple-darwin"; +} + +#[cfg(target_os = "linux")] +impl TyLspAdapter { + const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz; + const ARCH_SERVER_NAME: &str = "unknown-linux-gnu"; +} + +#[cfg(target_os = "freebsd")] +impl TyLspAdapter { + const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz; + const ARCH_SERVER_NAME: &str = "unknown-freebsd"; +} + +#[cfg(target_os = "windows")] +impl TyLspAdapter { + const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip; + const ARCH_SERVER_NAME: &str = "pc-windows-msvc"; +} + +impl TyLspAdapter { + const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ty"); + + pub fn new(fs: Arc) -> TyLspAdapter { + TyLspAdapter { fs } + } + + fn build_asset_name() -> Result<(String, String)> { + let arch = match consts::ARCH { + "x86" => "i686", + _ => consts::ARCH, + }; + let os = Self::ARCH_SERVER_NAME; + let suffix = match consts::OS { + "windows" => "zip", + _ => "tar.gz", + }; + let asset_name = format!("ty-{arch}-{os}.{suffix}"); + let asset_stem = format!("ty-{arch}-{os}"); + Ok((asset_stem, asset_name)) + } +} + +#[async_trait(?Send)] +impl LspAdapter for TyLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + _: &AsyncApp, + ) -> Result> { + let release = + latest_github_release("astral-sh/ty", true, true, delegate.http_client()).await?; + let (_, asset_name) = Self::build_asset_name()?; + let asset = release + .assets + .into_iter() + .find(|asset| asset.name == asset_name) + .with_context(|| format!("no asset found matching `{asset_name:?}`"))?; + Ok(Box::new(GitHubLspBinaryVersion { + name: release.tag_name, + url: asset.browser_download_url, + digest: asset.digest, + })) + } + + async fn fetch_server_binary( + &self, + latest_version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let GitHubLspBinaryVersion { + name, + url, + digest: expected_digest, + } = *latest_version.downcast::().unwrap(); + let destination_path = container_dir.join(format!("ty-{name}")); + let server_path = match Self::GITHUB_ASSET_KIND { + AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place. + AssetKind::Zip => destination_path.clone().join("ty.exe"), // zip contains a .exe + }; + + let binary = LanguageServerBinary { + path: server_path.clone(), + env: None, + arguments: Default::default(), + }; + + let metadata_path = destination_path.with_extension("metadata"); + let metadata = GithubBinaryMetadata::read_from_file(&metadata_path) + .await + .ok(); + if let Some(metadata) = metadata { + let validity_check = async || { + delegate + .try_exec(LanguageServerBinary { + path: server_path.clone(), + arguments: vec!["--version".into()], + env: None, + }) + .await + .inspect_err(|err| { + log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",) + }) + }; + if let (Some(actual_digest), Some(expected_digest)) = + (&metadata.digest, &expected_digest) + { + if actual_digest == expected_digest { + if validity_check().await.is_ok() { + return Ok(binary); + } + } else { + log::info!( + "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}" + ); + } + } else if validity_check().await.is_ok() { + return Ok(binary); + } + } + + download_server_binary( + delegate, + &url, + expected_digest.as_deref(), + &destination_path, + Self::GITHUB_ASSET_KIND, + ) + .await?; + make_file_executable(&server_path).await?; + remove_matching(&container_dir, |path| path != destination_path).await; + GithubBinaryMetadata::write_to_file( + &GithubBinaryMetadata { + metadata_version: 1, + digest: expected_digest, + }, + &metadata_path, + ) + .await?; + + Ok(LanguageServerBinary { + path: server_path, + env: None, + arguments: Default::default(), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + maybe!(async { + let mut last = None; + let mut entries = self.fs.read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let path = entry?; + if path.extension().is_some_and(|ext| ext == "metadata") { + continue; + } + last = Some(path); + } + + let path = last.context("no cached binary")?; + let path = match TyLspAdapter::GITHUB_ASSET_KIND { + AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place. + AssetKind::Zip => path.join("ty.exe"), // zip contains a .exe + }; + + anyhow::Ok(LanguageServerBinary { + path, + env: None, + arguments: Default::default(), + }) + }) + .await + .log_err() + } + + async fn workspace_configuration( + self: Arc, + _: &Arc, + toolchain: Option, + _cx: &mut AsyncApp, + ) -> Result { + let mut ret = json!({}); + if let Some(toolchain) = toolchain.and_then(|toolchain| { + serde_json::from_value::(toolchain.as_json).ok() + }) { + _ = maybe!({ + let uri = url::Url::from_file_path(toolchain.executable?).ok()?; + let sys_prefix = toolchain.prefix.clone()?; + let environment = json!({ + "executable": { + "uri": uri, + "sysPrefix": sys_prefix + } + }); + ret.as_object_mut()?.insert( + "pythonExtension".into(), + json!({ "activeEnvironment": environment }), + ); + Some(()) + }); + } + Ok(json!({"ty": ret})) + } +} + +pub struct PyrightLspAdapter { node: NodeRuntime, } -impl PythonLspAdapter { +impl PyrightLspAdapter { const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright"); pub fn new(node: NodeRuntime) -> Self { - PythonLspAdapter { node } + PyrightLspAdapter { node } } } #[async_trait(?Send)] -impl LspAdapter for PythonLspAdapter { +impl LspAdapter for PyrightLspAdapter { fn name(&self) -> LanguageServerName { Self::SERVER_NAME }