Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use command::r#async::Command;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::install::{
|
||||
fetch_latest_metadata_from_github_dynamic_asset, install_from_github, AssetKind,
|
||||
};
|
||||
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
||||
use crate::CommandBuilder;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
const SERVER_NAME: &str = "clangd";
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
||||
pub struct ClangdCandidate {
|
||||
client: Arc<http_client::Client>,
|
||||
}
|
||||
|
||||
impl ClangdCandidate {
|
||||
pub fn new(client: Arc<http_client::Client>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub async fn find_installed_binary_in_data_dir() -> Option<PathBuf> {
|
||||
let install_root = warp_core::paths::data_dir().join(SERVER_NAME);
|
||||
if !install_root.is_dir() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(&install_root) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let version_dir = entry.path();
|
||||
if !version_dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(binary_path) = find_binary_in_dir(&version_dir) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if binary_is_working(&binary_path).await {
|
||||
return Some(binary_path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn asset_os_suffix() -> anyhow::Result<&'static str> {
|
||||
match (std::env::consts::OS, std::env::consts::ARCH) {
|
||||
("macos", _) => Ok("mac"),
|
||||
("linux", "x86_64") => Ok("linux"),
|
||||
("windows", "x86_64") => Ok("windows"),
|
||||
(os, arch) => anyhow::bail!("Unsupported platform for clangd: {os}/{arch}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn is_c_or_cpp_extension(extension: &str) -> bool {
|
||||
matches!(
|
||||
extension,
|
||||
"c" | "C" | "cc" | "cpp" | "cxx" | "h" | "hh" | "hpp" | "hxx" | "H"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
async fn binary_is_working(binary_path: &Path) -> bool {
|
||||
let mut command = Command::new(binary_path);
|
||||
command.arg("--version");
|
||||
command
|
||||
.output()
|
||||
.await
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn is_bin_clangd_path(path: &Path) -> bool {
|
||||
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let expected_name = if cfg!(windows) {
|
||||
"clangd.exe"
|
||||
} else {
|
||||
"clangd"
|
||||
};
|
||||
|
||||
if file_name != expected_name {
|
||||
return false;
|
||||
}
|
||||
|
||||
path.parent()
|
||||
.and_then(|parent| parent.file_name())
|
||||
.and_then(|name| name.to_str())
|
||||
== Some("bin")
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn find_binary_in_dir(root: &Path) -> Option<PathBuf> {
|
||||
let mut directories = vec![root.to_path_buf()];
|
||||
|
||||
while let Some(dir) = directories.pop() {
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
directories.push(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_bin_clangd_path(&path) {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "local_fs")]
|
||||
impl LanguageServerCandidate for ClangdCandidate {
|
||||
async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
let repo_markers = [
|
||||
"compile_commands.json",
|
||||
"compile_flags.txt",
|
||||
".clangd",
|
||||
"CMakeLists.txt",
|
||||
];
|
||||
|
||||
if repo_markers.iter().any(|marker| path.join(marker).exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(path) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
entries.flatten().any(|entry| {
|
||||
let file_path = entry.path();
|
||||
file_path.is_file()
|
||||
&& file_path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_some_and(is_c_or_cpp_extension)
|
||||
})
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
Self::find_installed_binary_in_data_dir().await.is_some()
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool {
|
||||
executor
|
||||
.command(SERVER_NAME)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
let binary_path = install_from_github(
|
||||
&self.client,
|
||||
&metadata,
|
||||
SERVER_NAME,
|
||||
AssetKind::Zip,
|
||||
Some(find_binary_in_dir),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Verify the installed binary works
|
||||
if !binary_is_working(&binary_path).await {
|
||||
anyhow::bail!(
|
||||
"Installed clangd binary at {} failed version check",
|
||||
binary_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
let os_suffix = asset_os_suffix()?;
|
||||
|
||||
fetch_latest_metadata_from_github_dynamic_asset(
|
||||
&self.client,
|
||||
"clangd",
|
||||
"clangd",
|
||||
move |tag| format!("clangd-{os_suffix}-{tag}.zip"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
impl LanguageServerCandidate for ClangdCandidate {
|
||||
async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
_metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
||||
use crate::CommandBuilder;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::install::fetch_latest_metadata_from_github;
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
||||
pub struct GoPlsCandidate {
|
||||
client: Arc<http_client::Client>,
|
||||
}
|
||||
|
||||
impl GoPlsCandidate {
|
||||
pub fn new(client: Arc<http_client::Client>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "local_fs")]
|
||||
impl LanguageServerCandidate for GoPlsCandidate {
|
||||
async fn should_suggest_for_repo(&self, path: &Path, executor: &CommandBuilder) -> bool {
|
||||
if !path.join("go.mod").exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if Go is installed
|
||||
executor
|
||||
.command("go")
|
||||
.arg("version")
|
||||
.output()
|
||||
.await
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
// gopls doesn't support custom installation yet
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool {
|
||||
executor
|
||||
.command("gopls")
|
||||
.arg("version")
|
||||
.output()
|
||||
.await
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
_metadata: LanguageServerMetadata,
|
||||
executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
let output = executor
|
||||
.command("go")
|
||||
.args(["install", "golang.org/x/tools/gopls@latest"])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("Failed to install gopls: {}", stderr);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
// gopls doesn't provide prebuilt binaries; it must be installed via `go install`
|
||||
fetch_latest_metadata_from_github(&self.client, "golang", "tools", None).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
impl LanguageServerCandidate for GoPlsCandidate {
|
||||
async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
_metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod clangd;
|
||||
pub mod go;
|
||||
pub mod pyright;
|
||||
pub mod rust;
|
||||
pub mod typescript_language_server;
|
||||
@@ -0,0 +1,234 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::supported_servers::CustomBinaryConfig;
|
||||
use crate::CommandBuilder;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use anyhow::Context;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use command::r#async::Command;
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
||||
pub struct PyrightCandidate {
|
||||
client: Arc<http_client::Client>,
|
||||
}
|
||||
|
||||
impl PyrightCandidate {
|
||||
/// Path to the langserver JS file relative to the pyright install directory.
|
||||
#[cfg(feature = "local_fs")]
|
||||
const LANGSERVER_JS_PATH: &str = "node_modules/pyright/langserver.index.js";
|
||||
|
||||
/// Path to the pyright CLI JS file (used for version checks).
|
||||
#[cfg(feature = "local_fs")]
|
||||
const PYRIGHT_CLI_PATH: &str = "node_modules/pyright/dist/pyright.js";
|
||||
|
||||
pub fn new(client: Arc<http_client::Client>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Finds the configuration for running pyright from our custom installation.
|
||||
///
|
||||
/// Instead of running the `pyright-langserver` wrapper script (which has a shebang
|
||||
/// requiring node in PATH), we run node directly with the langserver.index.js file.
|
||||
/// This is the same pattern used by Zed.
|
||||
///
|
||||
/// Pyright can be installed with either system node or our custom node. This function
|
||||
/// checks for both cases:
|
||||
/// 1. First tries our custom node installation
|
||||
/// 2. Falls back to system node if custom node isn't available
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path_env_var` - The PATH environment variable to use when checking for system node.
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub async fn find_installed_binary_config(
|
||||
path_env_var: Option<&str>,
|
||||
) -> Option<CustomBinaryConfig> {
|
||||
let install_dir = warp_core::paths::data_dir().join("pyright");
|
||||
let langserver_js = install_dir.join(Self::LANGSERVER_JS_PATH);
|
||||
|
||||
// Check if the JS file exists
|
||||
if !langserver_js.is_file() {
|
||||
log::info!(
|
||||
"Pyright langserver.index.js not found at {}",
|
||||
langserver_js.display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Try to find a working node binary - first custom, then system
|
||||
let node_binary = node_runtime::find_working_node_binary(path_env_var).await?;
|
||||
|
||||
// Verify the pyright installation works by running `node pyright.js --version`
|
||||
let pyright_cli = install_dir.join(Self::PYRIGHT_CLI_PATH);
|
||||
if pyright_cli.is_file() {
|
||||
let mut cmd = Command::new(&node_binary);
|
||||
// Propagate PATH so "node" (bare name) resolves when using system node
|
||||
if let Some(path) = path_env_var {
|
||||
cmd.env("PATH", path);
|
||||
}
|
||||
cmd.arg(&pyright_cli).arg("--version");
|
||||
match cmd.output().await {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout);
|
||||
log::info!("Verified pyright installation: {}", version.trim());
|
||||
}
|
||||
Ok(output) => {
|
||||
log::warn!(
|
||||
"Pyright version check failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to run pyright version check: {}", e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Pyright CLI not found at {}, skipping version check",
|
||||
pyright_cli.display()
|
||||
);
|
||||
// Still proceed - the langserver.index.js exists, installation might still work
|
||||
}
|
||||
|
||||
Some(CustomBinaryConfig {
|
||||
binary_path: node_binary,
|
||||
prepend_args: vec![langserver_js.to_string_lossy().to_string()],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "local_fs")]
|
||||
impl LanguageServerCandidate for PyrightCandidate {
|
||||
async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
// Check for common Python project indicators
|
||||
path.join("pyproject.toml").exists()
|
||||
|| path.join("setup.py").exists()
|
||||
|| path.join("requirements.txt").exists()
|
||||
|| path.join("Pipfile").exists()
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, executor: &CommandBuilder) -> bool {
|
||||
Self::find_installed_binary_config(executor.path_env_var())
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool {
|
||||
executor
|
||||
.command("pyright-langserver")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
metadata: LanguageServerMetadata,
|
||||
executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!("Installing pyright version {}", metadata.version);
|
||||
|
||||
let install_dir = warp_core::paths::data_dir().join("pyright");
|
||||
|
||||
// Create the installation directory
|
||||
async_fs::create_dir_all(&install_dir)
|
||||
.await
|
||||
.context("Failed to create pyright installation directory")?;
|
||||
|
||||
// First, check if system node is available and meets requirements
|
||||
let use_system_node = match executor.path_env_var() {
|
||||
Some(path) => node_runtime::detect_system_node(path).await.is_ok(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
let custom_node_paths = if use_system_node {
|
||||
log::info!("Using system Node.js for pyright installation");
|
||||
None
|
||||
} else {
|
||||
log::info!("System Node.js not found or too old, installing custom Node.js");
|
||||
node_runtime::install_npm(&self.client).await?;
|
||||
Some((
|
||||
node_runtime::node_binary_path()?,
|
||||
node_runtime::npm_binary_path()?,
|
||||
))
|
||||
};
|
||||
|
||||
// Install pyright using npm
|
||||
log::info!("Installing pyright@{} using npm", metadata.version);
|
||||
|
||||
// Build the npm install command:
|
||||
// - System node: run `npm` directly (it's on PATH)
|
||||
// - Custom node: run `node <npm_path>` to avoid relying on shebang resolution
|
||||
let mut cmd = if let Some((node_path, npm_path)) = &custom_node_paths {
|
||||
let mut c = executor.command(node_path);
|
||||
c.arg(npm_path);
|
||||
c
|
||||
} else {
|
||||
executor.command("npm")
|
||||
};
|
||||
|
||||
cmd.arg("install")
|
||||
.arg("--ignore-scripts")
|
||||
.arg(format!("pyright@{}", metadata.version))
|
||||
.current_dir(&install_dir);
|
||||
|
||||
let output = cmd.output().await.context("Failed to run npm install")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("Failed to install pyright via npm: {}", stderr);
|
||||
}
|
||||
|
||||
log::info!("Pyright installed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
let version = node_runtime::fetch_npm_package_version(&self.client, "pyright")
|
||||
.await
|
||||
.context("Failed to fetch pyright version from npm registry")?;
|
||||
|
||||
Ok(LanguageServerMetadata {
|
||||
version,
|
||||
url: None, // npm packages don't have direct download URLs
|
||||
digest: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
impl LanguageServerCandidate for PyrightCandidate {
|
||||
async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
_metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::install::{fetch_latest_metadata_from_github, install_from_github, AssetKind};
|
||||
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
||||
use crate::CommandBuilder;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
||||
pub struct RustAnalyzerCandidate {
|
||||
client: Arc<http_client::Client>,
|
||||
}
|
||||
|
||||
/// Returns the rust-analyzer asset name for the current platform.
|
||||
///
|
||||
/// Asset names follow the pattern: rust-analyzer-{arch}-{vendor}-{os}.{ext}
|
||||
/// e.g. rust-analyzer-aarch64-apple-darwin.gz, rust-analyzer-x86_64-unknown-linux-gnu.gz
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn asset_name() -> &'static str {
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
{
|
||||
"rust-analyzer-aarch64-apple-darwin.gz"
|
||||
}
|
||||
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
||||
{
|
||||
"rust-analyzer-x86_64-apple-darwin.gz"
|
||||
}
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
{
|
||||
"rust-analyzer-x86_64-unknown-linux-gnu.gz"
|
||||
}
|
||||
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
|
||||
{
|
||||
"rust-analyzer-aarch64-unknown-linux-gnu.gz"
|
||||
}
|
||||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
||||
{
|
||||
"rust-analyzer-x86_64-pc-windows-msvc.zip"
|
||||
}
|
||||
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
|
||||
{
|
||||
"rust-analyzer-aarch64-pc-windows-msvc.zip"
|
||||
}
|
||||
#[cfg(not(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "macos", target_arch = "x86_64"),
|
||||
all(target_os = "linux", target_arch = "x86_64"),
|
||||
all(target_os = "linux", target_arch = "aarch64"),
|
||||
all(target_os = "windows", target_arch = "x86_64"),
|
||||
all(target_os = "windows", target_arch = "aarch64"),
|
||||
)))]
|
||||
{
|
||||
todo!("Unsupported platform for rust-analyzer")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
const SERVER_NAME: &str = "rust-analyzer";
|
||||
|
||||
impl RustAnalyzerCandidate {
|
||||
pub fn new(client: Arc<http_client::Client>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Finds the path to an installed rust-analyzer binary in the data directory.
|
||||
///
|
||||
/// Returns the path to the first working binary found (verified by running `--help`).
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub async fn find_installed_binary_in_data_dir() -> Option<std::path::PathBuf> {
|
||||
use tokio::process::Command;
|
||||
|
||||
let install_dir = warp_core::paths::data_dir().join(SERVER_NAME);
|
||||
if !install_dir.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check if any version directory contains a working binary
|
||||
let binary_name = if cfg!(windows) {
|
||||
format!("{}.exe", SERVER_NAME)
|
||||
} else {
|
||||
SERVER_NAME.to_string()
|
||||
};
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(&install_dir) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let binary_path = path.join(&binary_name);
|
||||
if binary_path.is_file() {
|
||||
// Verify the binary works by running --help
|
||||
let mut cmd = Command::new(&binary_path);
|
||||
cmd.arg("--help");
|
||||
if cmd
|
||||
.output()
|
||||
.await
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Some(binary_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "local_fs")]
|
||||
impl LanguageServerCandidate for RustAnalyzerCandidate {
|
||||
async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
path.join("Cargo.toml").exists()
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
Self::find_installed_binary_in_data_dir().await.is_some()
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool {
|
||||
executor
|
||||
.command(SERVER_NAME)
|
||||
.arg("--help")
|
||||
.output()
|
||||
.await
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
let asset_kind = AssetKind::from_filename(asset_name()).ok_or_else(|| {
|
||||
anyhow::anyhow!("Unsupported archive format for asset: {}", asset_name())
|
||||
})?;
|
||||
install_from_github(&self.client, &metadata, SERVER_NAME, asset_kind, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
fetch_latest_metadata_from_github(
|
||||
&self.client,
|
||||
"rust-lang",
|
||||
"rust-analyzer",
|
||||
Some(asset_name()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
impl LanguageServerCandidate for RustAnalyzerCandidate {
|
||||
async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
_metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::supported_servers::CustomBinaryConfig;
|
||||
use crate::CommandBuilder;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use anyhow::Context;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use command::r#async::Command;
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
||||
pub struct TypeScriptLanguageServerCandidate {
|
||||
client: Arc<http_client::Client>,
|
||||
}
|
||||
|
||||
impl TypeScriptLanguageServerCandidate {
|
||||
/// Path to the new langserver JS file (v4.0.0+) relative to the install directory.
|
||||
#[cfg(feature = "local_fs")]
|
||||
const NEW_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.mjs";
|
||||
|
||||
/// Path to the old langserver JS file (pre-4.0.0) relative to the install directory.
|
||||
#[cfg(feature = "local_fs")]
|
||||
const OLD_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.js";
|
||||
|
||||
pub fn new(client: Arc<http_client::Client>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Finds the configuration for running typescript-language-server from our custom installation.
|
||||
///
|
||||
/// Instead of running the wrapper script (which has a shebang requiring node in PATH),
|
||||
/// we run node directly with the CLI JS file. This is the same pattern used by Zed.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path_env_var` - The PATH environment variable to use when checking for system node.
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub async fn find_installed_binary_config(
|
||||
path_env_var: Option<&str>,
|
||||
) -> Option<CustomBinaryConfig> {
|
||||
let install_dir = warp_core::paths::data_dir().join("typescript-language-server");
|
||||
|
||||
// Check for the JS file - prefer new path (cli.mjs) over old path (cli.js)
|
||||
let server_js = {
|
||||
let new_path = install_dir.join(Self::NEW_SERVER_PATH);
|
||||
if new_path.is_file() {
|
||||
new_path
|
||||
} else {
|
||||
let old_path = install_dir.join(Self::OLD_SERVER_PATH);
|
||||
if old_path.is_file() {
|
||||
old_path
|
||||
} else {
|
||||
log::info!(
|
||||
"typescript-language-server JS file not found at {} or {}",
|
||||
new_path.display(),
|
||||
old_path.display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Try to find a working node binary - first custom, then system
|
||||
let node_binary = node_runtime::find_working_node_binary(path_env_var).await?;
|
||||
|
||||
// Verify the installation works by running `node cli.mjs --version`
|
||||
let mut cmd = Command::new(&node_binary);
|
||||
// Propagate PATH so "node" (bare name) resolves when using system node
|
||||
if let Some(path) = path_env_var {
|
||||
cmd.env("PATH", path);
|
||||
}
|
||||
cmd.arg(&server_js).arg("--version");
|
||||
match cmd.output().await {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout);
|
||||
log::info!(
|
||||
"Verified typescript-language-server installation: {}",
|
||||
version.trim()
|
||||
);
|
||||
}
|
||||
Ok(output) => {
|
||||
log::warn!(
|
||||
"typescript-language-server version check failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to run typescript-language-server version check: {}",
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(CustomBinaryConfig {
|
||||
binary_path: node_binary,
|
||||
prepend_args: vec![server_js.to_string_lossy().to_string()],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "local_fs")]
|
||||
impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
|
||||
async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
// Check for common JavaScript/TypeScript project indicators
|
||||
path.join("package.json").exists()
|
||||
|| path.join("tsconfig.json").exists()
|
||||
|| path.join("jsconfig.json").exists()
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, executor: &CommandBuilder) -> bool {
|
||||
Self::find_installed_binary_config(executor.path_env_var())
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool {
|
||||
executor
|
||||
.command("typescript-language-server")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
metadata: LanguageServerMetadata,
|
||||
executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!(
|
||||
"Installing typescript-language-server version {}",
|
||||
metadata.version
|
||||
);
|
||||
|
||||
let install_dir = warp_core::paths::data_dir().join("typescript-language-server");
|
||||
|
||||
// Create the installation directory
|
||||
async_fs::create_dir_all(&install_dir)
|
||||
.await
|
||||
.context("Failed to create typescript-language-server installation directory")?;
|
||||
|
||||
// First, check if system node is available and meets requirements
|
||||
let use_system_node = match executor.path_env_var() {
|
||||
Some(path) => node_runtime::detect_system_node(path).await.is_ok(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
let custom_node_paths = if use_system_node {
|
||||
log::info!("Using system Node.js for typescript-language-server installation");
|
||||
None
|
||||
} else {
|
||||
log::info!("System Node.js not found or too old, installing custom Node.js");
|
||||
node_runtime::install_npm(&self.client).await?;
|
||||
Some((
|
||||
node_runtime::node_binary_path()?,
|
||||
node_runtime::npm_binary_path()?,
|
||||
))
|
||||
};
|
||||
|
||||
// Install typescript-language-server and typescript using npm
|
||||
// typescript is a peer dependency required for the language server to work
|
||||
log::info!(
|
||||
"Installing typescript-language-server@{} using npm",
|
||||
metadata.version
|
||||
);
|
||||
|
||||
// Build the npm install command:
|
||||
// - System node: run `npm` directly (it's on PATH)
|
||||
// - Custom node: run `node <npm_path>` to avoid relying on shebang resolution
|
||||
let mut cmd = if let Some((node_path, npm_path)) = &custom_node_paths {
|
||||
let mut c = executor.command(node_path);
|
||||
c.arg(npm_path);
|
||||
c
|
||||
} else {
|
||||
executor.command("npm")
|
||||
};
|
||||
|
||||
cmd.arg("install")
|
||||
.arg("--ignore-scripts")
|
||||
.arg(format!("typescript-language-server@{}", metadata.version))
|
||||
.arg("typescript")
|
||||
.current_dir(&install_dir);
|
||||
|
||||
let output = cmd.output().await.context("Failed to run npm install")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!(
|
||||
"Failed to install typescript-language-server via npm: {}",
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
log::info!("typescript-language-server installed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
let version =
|
||||
node_runtime::fetch_npm_package_version(&self.client, "typescript-language-server")
|
||||
.await
|
||||
.context("Failed to fetch typescript-language-server version from npm registry")?;
|
||||
|
||||
Ok(LanguageServerMetadata {
|
||||
version,
|
||||
url: None, // npm packages don't have direct download URLs
|
||||
digest: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
|
||||
async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn install(
|
||||
&self,
|
||||
_metadata: LanguageServerMetadata,
|
||||
_executor: &CommandBuilder,
|
||||
) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user