Split Lsp installation methods from LspAdapter

This commit is contained in:
Lukas Wirth 2025-08-06 11:48:05 +02:00
parent 85865fc950
commit aae59a2842
13 changed files with 719 additions and 717 deletions

View file

@ -8,7 +8,7 @@ use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName};
use project::lsp_store::clangd_ext;
use serde_json::json;
use smol::fs;
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
use std::{env::consts, path::PathBuf, sync::Arc};
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
use crate::github_download::{GithubBinaryMetadata, download_server_binary};
@ -19,30 +19,13 @@ impl CLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd");
}
#[async_trait(?Send)]
impl super::LspAdapter for CLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
Some(LanguageServerBinary {
path,
arguments: Vec::new(),
env: None,
})
}
impl LspInstaller for CLspAdapter {
type BinaryVersion = GitHubLspBinaryVersion;
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<GitHubLspBinaryVersion> {
let release =
latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?;
let os_suffix = match consts::OS {
@ -62,12 +45,26 @@ impl super::LspAdapter for CLspAdapter {
url: asset.browser_download_url.clone(),
digest: asset.digest.clone(),
};
Ok(Box::new(version) as Box<_>)
Ok(version)
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
Some(LanguageServerBinary {
path,
arguments: Vec::new(),
env: None,
})
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
version: GitHubLspBinaryVersion,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
@ -75,7 +72,7 @@ impl super::LspAdapter for CLspAdapter {
name,
url,
digest: expected_digest,
} = *version.downcast::<GitHubLspBinaryVersion>().unwrap();
} = version;
let version_dir = container_dir.join(format!("clangd_{name}"));
let binary_path = version_dir.join("bin/clangd");
@ -146,6 +143,13 @@ impl super::LspAdapter for CLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
}
}
#[async_trait(?Send)]
impl super::LspAdapter for CLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn label_for_completion(
&self,

View file

@ -2,14 +2,13 @@ use anyhow::{Context as _, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::AsyncApp;
use language::{LspAdapter, LspAdapterDelegate, Toolchain};
use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::json;
use smol::fs;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
@ -34,10 +33,13 @@ impl CssLspAdapter {
}
}
#[async_trait(?Send)]
impl LspAdapter for CssLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-css-language-server".into())
impl LspInstaller for CssLspAdapter {
type BinaryVersion = String;
async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<String> {
self.node
.npm_package_latest_version("vscode-langservers-extracted")
.await
}
async fn check_if_user_installed(
@ -58,24 +60,12 @@ impl LspAdapter for CssLspAdapter {
})
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
.npm_package_latest_version("vscode-langservers-extracted")
.await?,
) as Box<_>)
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: String,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
self.node
@ -94,11 +84,10 @@ impl LspAdapter for CssLspAdapter {
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
version: &String,
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
@ -129,6 +118,13 @@ impl LspAdapter for CssLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &self.node).await
}
}
#[async_trait(?Send)]
impl LspAdapter for CssLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-css-language-server".into())
}
async fn initialization_options(
self: Arc<Self>,

View file

@ -5,17 +5,17 @@ use futures::StreamExt;
use gpui::{App, AsyncApp, Task};
use http_client::github::latest_github_release;
pub use language::*;
use language::{LanguageToolchainStore, LspAdapterDelegate, LspInstaller};
use lsp::{LanguageServerBinary, LanguageServerName};
use project::Fs;
use regex::Regex;
use serde_json::json;
use smol::fs;
use std::{
any::Any,
borrow::Cow,
ffi::{OsStr, OsString},
ops::Range,
path::PathBuf,
path::{Path, PathBuf},
process::Output,
str,
sync::{
@ -50,16 +50,13 @@ const BINARY: &str = if cfg!(target_os = "windows") {
"gopls"
};
#[async_trait(?Send)]
impl super::LspAdapter for GoLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
impl LspInstaller for GoLspAdapter {
type BinaryVersion = Option<String>;
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Option<String>> {
let release =
latest_github_release("golang/tools", false, false, delegate.http_client()).await?;
let version: Option<String> = release.tag_name.strip_prefix("gopls/v").map(str::to_string);
@ -69,7 +66,7 @@ impl super::LspAdapter for GoLspAdapter {
release.tag_name
);
}
Ok(Box::new(version) as Box<_>)
Ok(version)
}
async fn check_if_user_installed(
@ -115,7 +112,7 @@ impl super::LspAdapter for GoLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
version: Option<String>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
@ -126,10 +123,8 @@ impl super::LspAdapter for GoLspAdapter {
.await
.context("failed to get go version via `go version` command`")?;
let go_version = parse_version_output(&go_version_output)?;
let version = version.downcast::<Option<String>>().unwrap();
let this = *self;
if let Some(version) = *version {
if let Some(version) = version {
let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}"));
if let Ok(metadata) = fs::metadata(&binary_path).await
&& metadata.is_file()
@ -145,10 +140,7 @@ impl super::LspAdapter for GoLspAdapter {
env: None,
});
}
} else if let Some(path) = this
.cached_server_binary(container_dir.clone(), delegate)
.await
{
} else if let Some(path) = get_cached_server_binary(&container_dir).await {
return Ok(path);
}
@ -194,7 +186,14 @@ impl super::LspAdapter for GoLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
get_cached_server_binary(&container_dir).await
}
}
#[async_trait(?Send)]
impl LspAdapter for GoLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn initialization_options(
@ -442,10 +441,10 @@ fn parse_version_output(output: &Output) -> Result<&str> {
Ok(version)
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async fn get_cached_server_binary(container_dir: &Path) -> Option<LanguageServerBinary> {
maybe!(async {
let mut last_binary_path = None;
let mut entries = fs::read_dir(&container_dir).await?;
let mut entries = fs::read_dir(container_dir).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
if entry.file_type().await?.is_file()

View file

@ -9,7 +9,7 @@ use gpui::{App, AsyncApp, Task};
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
use language::{
ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter,
LspAdapterDelegate, Toolchain,
LspAdapterDelegate, LspInstaller, Toolchain,
};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
@ -22,7 +22,6 @@ use smol::{
lock::RwLock,
};
use std::{
any::Any,
env::consts,
ffi::OsString,
path::{Path, PathBuf},
@ -294,10 +293,13 @@ fn generate_inspector_style_schema() -> serde_json_lenient::Value {
serde_json_lenient::to_value(schema).unwrap()
}
#[async_trait(?Send)]
impl LspAdapter for JsonLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("json-language-server".into())
impl LspInstaller for JsonLspAdapter {
type BinaryVersion = String;
async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<String> {
self.node
.npm_package_latest_version(Self::PACKAGE_NAME)
.await
}
async fn check_if_user_installed(
@ -318,24 +320,12 @@ impl LspAdapter for JsonLspAdapter {
})
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(
self.node
.npm_package_latest_version(Self::PACKAGE_NAME)
.await?,
) as Box<_>)
}
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
version: &String,
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
@ -361,11 +351,10 @@ impl LspAdapter for JsonLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: String,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
self.node
@ -389,6 +378,13 @@ impl LspAdapter for JsonLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &self.node).await
}
}
#[async_trait(?Send)]
impl LspAdapter for JsonLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("json-language-server".into())
}
async fn initialization_options(
self: Arc<Self>,
@ -485,16 +481,13 @@ impl NodeVersionAdapter {
LanguageServerName::new_static("package-version-server");
}
#[async_trait(?Send)]
impl LspAdapter for NodeVersionAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
impl LspInstaller for NodeVersionAdapter {
type BinaryVersion = GitHubLspBinaryVersion;
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<GitHubLspBinaryVersion> {
let release = latest_github_release(
"zed-industries/package-version-server",
true,
@ -519,11 +512,11 @@ impl LspAdapter for NodeVersionAdapter {
.iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
Ok(Box::new(GitHubLspBinaryVersion {
Ok(GitHubLspBinaryVersion {
name: release.tag_name,
url: asset.browser_download_url.clone(),
digest: asset.digest.clone(),
}))
})
}
async fn check_if_user_installed(
@ -542,11 +535,11 @@ impl LspAdapter for NodeVersionAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: GitHubLspBinaryVersion,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = latest_version.downcast::<GitHubLspBinaryVersion>().unwrap();
let version = &latest_version;
let destination_path = container_dir.join(format!(
"{}-{}{}",
Self::SERVER_NAME,
@ -596,6 +589,13 @@ impl LspAdapter for NodeVersionAdapter {
}
}
#[async_trait(?Send)]
impl LspAdapter for NodeVersionAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
}
async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
maybe!(async {
let mut last = None;

View file

@ -9,7 +9,7 @@ use language::ToolchainList;
use language::ToolchainLister;
use language::language_settings::language_settings;
use language::{ContextLocation, LanguageToolchainStore};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate, LspInstaller};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
use lsp::LanguageServerBinary;
use lsp::LanguageServerName;
@ -26,7 +26,6 @@ use std::cmp::Ordering;
use parking_lot::Mutex;
use std::str::FromStr;
use std::{
any::Any,
borrow::Cow,
ffi::OsString,
fmt::Write,
@ -100,28 +99,13 @@ impl PythonLspAdapter {
}
}
#[async_trait(?Send)]
impl LspAdapter for PythonLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
impl LspInstaller for PythonLspAdapter {
type BinaryVersion = String;
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<Value>> {
// Provide minimal initialization options
// Virtual environment configuration will be handled through workspace configuration
Ok(Some(json!({
"python": {
"analysis": {
"autoSearchPaths": true,
"useLibraryCodeForTypes": true,
"autoImportCompletions": true
}
}
})))
async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<String> {
self.node
.npm_package_latest_version(Self::SERVER_NAME.as_ref())
.await
}
async fn check_if_user_installed(
@ -155,24 +139,12 @@ impl LspAdapter for PythonLspAdapter {
}
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
.npm_package_latest_version(Self::SERVER_NAME.as_ref())
.await?,
) as Box<_>)
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: String,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
self.node
@ -192,11 +164,10 @@ impl LspAdapter for PythonLspAdapter {
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
version: &String,
container_dir: &PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
@ -230,6 +201,31 @@ impl LspAdapter for PythonLspAdapter {
binary.env = Some(delegate.shell_env().await);
Some(binary)
}
}
#[async_trait(?Send)]
impl LspAdapter for PythonLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<Value>> {
// Provide minimal initialization options
// Virtual environment configuration will be handled through workspace configuration
Ok(Some(json!({
"python": {
"analysis": {
"autoSearchPaths": true,
"useLibraryCodeForTypes": true,
"autoImportCompletions": true
}
}
})))
}
async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
@ -1023,10 +1019,11 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") {
"bin"
};
#[async_trait(?Send)]
impl LspAdapter for PyLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
impl LspInstaller for PyLspAdapter {
type BinaryVersion = ();
async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<()> {
Ok(())
}
async fn check_if_user_installed(
@ -1053,16 +1050,9 @@ impl LspAdapter for PyLspAdapter {
}
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(()) as Box<_>)
}
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: (),
_: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
@ -1122,6 +1112,13 @@ impl LspAdapter for PyLspAdapter {
arguments: vec![],
})
}
}
#[async_trait(?Send)]
impl LspAdapter for PyLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {}
@ -1315,28 +1312,11 @@ impl BasedPyrightLspAdapter {
}
}
#[async_trait(?Send)]
impl LspAdapter for BasedPyrightLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
impl LspInstaller for BasedPyrightLspAdapter {
type BinaryVersion = ();
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<Value>> {
// Provide minimal initialization options
// Virtual environment configuration will be handled through workspace configuration
Ok(Some(json!({
"python": {
"analysis": {
"autoSearchPaths": true,
"useLibraryCodeForTypes": true,
"autoImportCompletions": true
}
}
})))
async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<()> {
Ok(())
}
async fn check_if_user_installed(
@ -1364,16 +1344,9 @@ impl LspAdapter for BasedPyrightLspAdapter {
}
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(()) as Box<_>)
}
async fn fetch_server_binary(
&self,
_latest_version: Box<dyn 'static + Send + Any>,
_latest_version: (),
_container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
@ -1411,6 +1384,31 @@ impl LspAdapter for BasedPyrightLspAdapter {
arguments: vec!["--stdio".into()],
})
}
}
#[async_trait(?Send)]
impl LspAdapter for BasedPyrightLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<Value>> {
// Provide minimal initialization options
// Virtual environment configuration will be handled through workspace configuration
Ok(Some(json!({
"python": {
"analysis": {
"autoSearchPaths": true,
"useLibraryCodeForTypes": true,
"autoImportCompletions": true
}
}
})))
}
async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.

View file

@ -17,7 +17,6 @@ use smol::fs::{self};
use std::fmt::Display;
use std::ops::Range;
use std::{
any::Any,
borrow::Cow,
path::{Path, PathBuf},
sync::{Arc, LazyLock},
@ -109,156 +108,6 @@ impl LspAdapter for RustLspAdapter {
SERVER_NAME
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which("rust-analyzer".as_ref()).await?;
let env = delegate.shell_env().await;
// It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
// /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
let result = delegate
.try_exec(LanguageServerBinary {
path: path.clone(),
arguments: vec!["--help".into()],
env: Some(env.clone()),
})
.await;
if let Err(err) = result {
log::debug!(
"failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
path,
err
);
return None;
}
Some(LanguageServerBinary {
path,
env: Some(env),
arguments: vec![],
})
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release(
"rust-lang/rust-analyzer",
true,
false,
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,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let GitHubLspBinaryVersion {
name,
url,
digest: expected_digest,
} = *version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path = container_dir.join(format!("rust-analyzer-{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("rust-analyzer.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<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
}
fn disk_based_diagnostic_sources(&self) -> Vec<String> {
vec![CARGO_DIAGNOSTICS_SOURCE_NAME.to_owned()]
}
@ -528,6 +377,159 @@ impl LspAdapter for RustLspAdapter {
}
}
impl LspInstaller for RustLspAdapter {
type BinaryVersion = GitHubLspBinaryVersion;
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which("rust-analyzer".as_ref()).await?;
let env = delegate.shell_env().await;
// It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
// /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
let result = delegate
.try_exec(LanguageServerBinary {
path: path.clone(),
arguments: vec!["--help".into()],
env: Some(env.clone()),
})
.await;
if let Err(err) = result {
log::debug!(
"failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
path,
err
);
return None;
}
Some(LanguageServerBinary {
path,
env: Some(env),
arguments: vec![],
})
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<GitHubLspBinaryVersion> {
let release = latest_github_release(
"rust-lang/rust-analyzer",
true,
false,
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(GitHubLspBinaryVersion {
name: release.tag_name,
url: asset.browser_download_url,
digest: asset.digest,
})
}
async fn fetch_server_binary(
&self,
version: GitHubLspBinaryVersion,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let GitHubLspBinaryVersion {
name,
url,
digest: expected_digest,
} = version;
let destination_path = container_dir.join(format!("rust-analyzer-{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("rust-analyzer.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<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
}
}
pub(crate) struct RustContextProvider;
const RUST_PACKAGE_TASK_VARIABLE: VariableName =

View file

@ -3,14 +3,13 @@ use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use gpui::AsyncApp;
use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::fs;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
@ -41,10 +40,13 @@ impl TailwindLspAdapter {
}
}
#[async_trait(?Send)]
impl LspAdapter for TailwindLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
impl LspInstaller for TailwindLspAdapter {
type BinaryVersion = String;
async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<String> {
self.node
.npm_package_latest_version(Self::PACKAGE_NAME)
.await
}
async fn check_if_user_installed(
@ -63,24 +65,12 @@ impl LspAdapter for TailwindLspAdapter {
})
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
.npm_package_latest_version(Self::PACKAGE_NAME)
.await?,
) as Box<_>)
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: String,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
self.node
@ -99,11 +89,10 @@ impl LspAdapter for TailwindLspAdapter {
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
version: &String,
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
@ -134,6 +123,13 @@ impl LspAdapter for TailwindLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &self.node).await
}
}
#[async_trait(?Send)]
impl LspAdapter for TailwindLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn initialization_options(
self: Arc<Self>,

View file

@ -7,7 +7,7 @@ use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate, Toolchain,
LspAdapterDelegate, LspInstaller, Toolchain,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
@ -15,7 +15,6 @@ use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::{fs, lock::RwLock, stream::StreamExt};
use std::{
any::Any,
borrow::Cow,
ffi::OsString,
path::{Path, PathBuf},
@ -549,37 +548,33 @@ impl TypeScriptLspAdapter {
}
}
struct TypeScriptVersions {
pub struct TypeScriptVersions {
typescript_version: String,
server_version: String,
}
#[async_trait(?Send)]
impl LspAdapter for TypeScriptLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
impl LspInstaller for TypeScriptLspAdapter {
type BinaryVersion = TypeScriptVersions;
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(TypeScriptVersions {
) -> Result<TypeScriptVersions> {
Ok(TypeScriptVersions {
typescript_version: self.node.npm_package_latest_version("typescript").await?,
server_version: self
.node
.npm_package_latest_version("typescript-language-server")
.await?,
}) as Box<_>)
})
}
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
version: &TypeScriptVersions,
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<TypeScriptVersions>().unwrap();
let server_path = container_dir.join(Self::NEW_SERVER_PATH);
let should_install_language_server = self
@ -605,11 +600,10 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: TypeScriptVersions,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
let server_path = container_dir.join(Self::NEW_SERVER_PATH);
self.node
@ -642,6 +636,13 @@ impl LspAdapter for TypeScriptLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_ts_server_binary(container_dir, &self.node).await
}
}
#[async_trait(?Send)]
impl LspAdapter for TypeScriptLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
Some(vec![
@ -809,6 +810,97 @@ impl EsLintLspAdapter {
}
}
impl LspInstaller for EsLintLspAdapter {
type BinaryVersion = GitHubLspBinaryVersion;
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
) -> Result<GitHubLspBinaryVersion> {
let url = build_asset_url(
"zed-industries/vscode-eslint",
Self::CURRENT_VERSION_TAG_NAME,
Self::GITHUB_ASSET_KIND,
)?;
Ok(GitHubLspBinaryVersion {
name: Self::CURRENT_VERSION.into(),
digest: None,
url,
})
}
async fn fetch_server_binary(
&self,
version: GitHubLspBinaryVersion,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let destination_path = Self::build_destination_path(&container_dir);
let server_path = destination_path.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
remove_matching(&container_dir, |_| true).await;
download_server_binary(
delegate,
&version.url,
None,
&destination_path,
Self::GITHUB_ASSET_KIND,
)
.await?;
let mut dir = fs::read_dir(&destination_path).await?;
let first = dir.next().await.context("missing first file")??;
let repo_root = destination_path.join("vscode-eslint");
fs::rename(first.path(), &repo_root).await?;
#[cfg(target_os = "windows")]
{
handle_symlink(
repo_root.join("$shared"),
repo_root.join("client").join("src").join("shared"),
)
.await?;
handle_symlink(
repo_root.join("$shared"),
repo_root.join("server").join("src").join("shared"),
)
.await?;
}
self.node
.run_npm_subcommand(&repo_root, "install", &[])
.await?;
self.node
.run_npm_subcommand(&repo_root, "run-script", &["compile"])
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let server_path =
Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
}
#[async_trait(?Send)]
impl LspAdapter for EsLintLspAdapter {
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@ -881,94 +973,6 @@ impl LspAdapter for EsLintLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let url = build_asset_url(
"zed-industries/vscode-eslint",
Self::CURRENT_VERSION_TAG_NAME,
Self::GITHUB_ASSET_KIND,
)?;
Ok(Box::new(GitHubLspBinaryVersion {
name: Self::CURRENT_VERSION.into(),
digest: None,
url,
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path = Self::build_destination_path(&container_dir);
let server_path = destination_path.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
remove_matching(&container_dir, |_| true).await;
download_server_binary(
delegate,
&version.url,
None,
&destination_path,
Self::GITHUB_ASSET_KIND,
)
.await?;
let mut dir = fs::read_dir(&destination_path).await?;
let first = dir.next().await.context("missing first file")??;
let repo_root = destination_path.join("vscode-eslint");
fs::rename(first.path(), &repo_root).await?;
#[cfg(target_os = "windows")]
{
handle_symlink(
repo_root.join("$shared"),
repo_root.join("client").join("src").join("shared"),
)
.await?;
handle_symlink(
repo_root.join("$shared"),
repo_root.join("server").join("src").join("shared"),
)
.await?;
}
self.node
.run_npm_subcommand(&repo_root, "install", &[])
.await?;
self.node
.run_npm_subcommand(&repo_root, "run-script", &["compile"])
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let server_path =
Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
}
#[cfg(target_os = "windows")]

View file

@ -2,13 +2,12 @@ use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
use gpui::AsyncApp;
use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::Value;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
@ -57,30 +56,27 @@ impl VtslsLspAdapter {
}
}
struct TypeScriptVersions {
pub struct TypeScriptVersions {
typescript_version: String,
server_version: String,
}
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
#[async_trait(?Send)]
impl LspAdapter for VtslsLspAdapter {
fn name(&self) -> LanguageServerName {
SERVER_NAME
}
impl LspInstaller for VtslsLspAdapter {
type BinaryVersion = TypeScriptVersions;
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(TypeScriptVersions {
) -> Result<TypeScriptVersions> {
Ok(TypeScriptVersions {
typescript_version: self.node.npm_package_latest_version("typescript").await?,
server_version: self
.node
.npm_package_latest_version("@vtsls/language-server")
.await?,
}) as Box<_>)
})
}
async fn check_if_user_installed(
@ -100,11 +96,10 @@ impl LspAdapter for VtslsLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: TypeScriptVersions,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
let server_path = container_dir.join(Self::SERVER_PATH);
let mut packages_to_install = Vec::new();
@ -156,6 +151,13 @@ impl LspAdapter for VtslsLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_ts_server_binary(container_dir, &self.node).await
}
}
#[async_trait(?Send)]
impl LspAdapter for VtslsLspAdapter {
fn name(&self) -> LanguageServerName {
SERVER_NAME
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
Some(vec![

View file

@ -2,7 +2,9 @@ use anyhow::{Context as _, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::AsyncApp;
use language::{LspAdapter, LspAdapterDelegate, Toolchain, language_settings::AllLanguageSettings};
use language::{
LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain, language_settings::AllLanguageSettings,
};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
@ -10,7 +12,6 @@ use serde_json::Value;
use settings::{Settings, SettingsLocation};
use smol::fs;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
@ -35,21 +36,13 @@ impl YamlLspAdapter {
}
}
#[async_trait(?Send)]
impl LspAdapter for YamlLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
impl LspInstaller for YamlLspAdapter {
type BinaryVersion = String;
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
.npm_package_latest_version("yaml-language-server")
.await?,
) as Box<_>)
async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<String> {
self.node
.npm_package_latest_version("yaml-language-server")
.await
}
async fn check_if_user_installed(
@ -70,11 +63,10 @@ impl LspAdapter for YamlLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: String,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
self.node
@ -93,11 +85,10 @@ impl LspAdapter for YamlLspAdapter {
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
version: &String,
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
@ -128,6 +119,13 @@ impl LspAdapter for YamlLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &self.node).await
}
}
#[async_trait(?Send)]
impl LspAdapter for YamlLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn workspace_configuration(
self: Arc<Self>,