Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+243
View File
@@ -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!()
}
}
+106
View File
@@ -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!()
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod clangd;
pub mod go;
pub mod pyright;
pub mod rust;
pub mod typescript_language_server;
+234
View File
@@ -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!()
}
}
+183
View File
@@ -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!()
}
}