Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions app/src/code/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ impl CodeFooterView {
fn render_server_row(server: &LspServerModel, appearance: &Appearance) -> Box<dyn Element> {
let render_status = LSPServerRenderStatus::render_status(Some(server));
let failed_error = match server.state() {
LspModelState::Failed { error } => Some(error.clone()),
LspModelState::Failed { error, .. } => Some(error.clone()),
_ => None,
};

Expand Down Expand Up @@ -1539,7 +1539,7 @@ impl CodeFooterView {
// Check for any failed server first
for server in &live {
let server_ref = server.as_ref(app);
if let LspModelState::Failed { error } = server_ref.state() {
if let LspModelState::Failed { error, .. } = server_ref.state() {
return (
Some(format!("{}: {error}", server_ref.server_name())),
false,
Expand Down
32 changes: 23 additions & 9 deletions app/src/settings_view/code_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use ai::index::full_source_code_embedding::manager::{
use ai::index::full_source_code_embedding::SyncProgress;
use ai::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
use ai::workspace::WorkspaceMetadata;
use lsp::supported_servers::{is_repairable_missing_binary_error, LSPServerType};
use lsp::supported_servers::{LSPServerType, LspStartupFailureReason};
use lsp::{LspManagerModel, LspManagerModelEvent, LspServerModel, LspState};
use pathfinder_color::ColorU;
use std::borrow::Cow;
Expand Down Expand Up @@ -2065,13 +2065,13 @@ impl CodePageWidget {
.into(),
Cow::Borrowed("Busy"),
),
LspState::Failed { error } => {
let status_text = if is_repairable_missing_binary_error(server_type, error)
{
Cow::Borrowed(TYPESCRIPT_LSP_MISSING_STATUS)
} else {
Cow::Borrowed("Failed")
};
LspState::Failed { failure_reason, .. } => {
let status_text =
if is_repairable_missing_binary_failure(server_type, *failure_reason) {
Cow::Borrowed(TYPESCRIPT_LSP_MISSING_STATUS)
} else {
Cow::Borrowed("Failed")
};
(
AnsiColorIdentifier::Red
.to_ansi_color(&theme.terminal_colors().normal)
Expand Down Expand Up @@ -2101,12 +2101,26 @@ impl CodePageWidget {
server_model.is_some_and(|model| {
matches!(
model.as_ref(app).state(),
LspState::Failed { error } if is_repairable_missing_binary_error(server_type, error)
LspState::Failed {
failure_reason, ..
} if is_repairable_missing_binary_failure(server_type, *failure_reason)
)
})
}
}

fn is_repairable_missing_binary_failure(
server_type: LSPServerType,
failure_reason: Option<LspStartupFailureReason>,
) -> bool {
matches!(
failure_reason,
Some(LspStartupFailureReason::MissingBinary {
server_type: failed_server_type,
}) if failed_server_type == server_type
)
}

/// A simple widget that renders a subheader title for a Code subpage.
struct CodeSubpageHeaderWidget {
title: &'static str,
Expand Down
1 change: 1 addition & 0 deletions crates/lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ lsp-types = "0.97.0"
node_runtime = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
rustls = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
warp_core = { workspace = true }
Expand Down
13 changes: 10 additions & 3 deletions crates/lsp/src/model.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
config::{lsp_uri_to_path, LanguageId},
server_repo_watcher::LspRepoWatcher,
supported_servers::LSPServerType,
supported_servers::{LSPServerType, LspStartupError, LspStartupFailureReason},
types::{
DefinitionLocation, DocumentVersion, HoverResult, Location, ReferenceLocation,
TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent,
Expand Down Expand Up @@ -71,6 +71,7 @@ pub enum LspState {
},
Failed {
error: String,
failure_reason: Option<LspStartupFailureReason>,
},
}

Expand Down Expand Up @@ -251,7 +252,7 @@ impl LspServerModel {
LspState::Starting => Err(anyhow::anyhow!("Server is starting")),
LspState::Stopped { .. } => Err(anyhow::anyhow!("Server is stopped")),
LspState::Stopping { .. } => Err(anyhow::anyhow!("Server is stopping")),
LspState::Failed { error } => Err(anyhow::anyhow!("Server has failed: {error}")),
LspState::Failed { error, .. } => Err(anyhow::anyhow!("Server has failed: {error}")),
}
}

Expand Down Expand Up @@ -312,8 +313,14 @@ impl LspServerModel {
}
Err(e) => {
log::error!("Failed to start LSP server: {e}");
let failure_reason = e
.downcast_ref::<LspStartupError>()
.map(|startup_error| startup_error.reason());
let error = format!("{e:#}");
me.server_state = LspState::Failed { error };
me.server_state = LspState::Failed {
error,
failure_reason,
};
ctx.emit(LspEvent::Failed(e));
}
},
Expand Down
136 changes: 136 additions & 0 deletions crates/lsp/src/servers/typescript_language_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
};

cmd.arg("install")
.arg("--prefix")
.arg(&install_dir)
.arg("--ignore-scripts")
.arg(format!("typescript-language-server@{}", metadata.version))
.arg("typescript")
Expand Down Expand Up @@ -343,3 +345,137 @@ impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
todo!()
}
}

#[cfg(all(test, feature = "local_fs"))]
mod smoke_tests {
use super::*;
use crate::config::LspServerConfig;
use crate::supported_servers::LSPServerType;
use crate::LspServiceInitializationResult;
use std::path::PathBuf;
use std::sync::{Arc, Once};
use warpui::r#async::executor::Background;

static INSTALL_CRYPTO_PROVIDER: Once = Once::new();

fn install_crypto_provider() {
INSTALL_CRYPTO_PROVIDER.call_once(|| {
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("failed to install default rustls crypto provider");
});
}

#[test]
fn smoke_workspace_local_typescript_lsp_from_env() {
install_crypto_provider();

let workspace_root = PathBuf::from(
std::env::var("CASTCODES_TS_LSP_SMOKE_REPO")
.expect("CASTCODES_TS_LSP_SMOKE_REPO is required"),
);
let path_env = std::env::var("PATH").ok();
let executor = Arc::new(Background::default());

warpui::r#async::block_on(async move {
let command_executor = crate::CommandBuilder::new(path_env.clone());
let workspace_config = TypeScriptLanguageServerCandidate::find_workspace_binary_config(
&workspace_root,
path_env.as_deref(),
&command_executor,
)
.await
.expect("workspace-local typescript-language-server should be discovered");

assert_eq!(
workspace_config.binary_path,
workspace_root.join(TypeScriptLanguageServerCandidate::LOCAL_BIN_PATH)
);
assert!(workspace_config.prepend_args.is_empty());

let config = LspServerConfig::new(
LSPServerType::TypeScriptLanguageServer,
workspace_root,
path_env,
"castcodes-smoke".to_owned(),
Arc::new(http_client::Client::new()),
);

let LspServiceInitializationResult { service, .. } =
crate::spawn_lsp_service(config, executor, None).await?;
service.shutdown().await?;
Ok::<_, anyhow::Error>(())
})
.unwrap();
}

#[test]
fn smoke_managed_typescript_lsp_install_from_env() {
install_crypto_provider();

let data_profile = match std::env::var("CAST_CODES_DATA_PROFILE") {
Ok(value) => value,
Err(_) => {
eprintln!(
"skipping smoke_managed_typescript_lsp_install_from_env: \
CAST_CODES_DATA_PROFILE is not set"
);
return;
}
};
assert!(
data_profile.contains("ts-lsp-smoke"),
"managed install smoke must use an isolated data profile"
);

let workspace_root = match std::env::var("CASTCODES_TS_LSP_MISSING_REPO") {
Ok(value) => PathBuf::from(value),
Err(_) => {
eprintln!(
"skipping smoke_managed_typescript_lsp_install_from_env: \
CASTCODES_TS_LSP_MISSING_REPO is not set"
);
return;
}
};
let path_env = std::env::var("PATH").ok();
let executor = Arc::new(Background::default());

warpui::r#async::block_on(async move {
let command_executor = crate::CommandBuilder::new(path_env.clone());
assert!(
TypeScriptLanguageServerCandidate::find_workspace_binary_config(
&workspace_root,
path_env.as_deref(),
&command_executor,
)
.await
.is_none(),
"missing workspace should not have local TypeScript LSP tooling"
);

let candidate =
TypeScriptLanguageServerCandidate::new(Arc::new(http_client::Client::new()));
let metadata = candidate.fetch_latest_server_metadata().await?;
candidate.install(metadata, &command_executor).await?;

TypeScriptLanguageServerCandidate::find_installed_binary_config(path_env.as_deref())
.await
.expect("managed TypeScript LSP install should be usable");

let config = LspServerConfig::new(
LSPServerType::TypeScriptLanguageServer,
workspace_root,
path_env,
"castcodes-smoke".to_owned(),
Arc::new(http_client::Client::new()),
);

let LspServiceInitializationResult { service, .. } =
crate::spawn_lsp_service(config, executor, None).await?;
service.shutdown().await?;
Ok::<_, anyhow::Error>(())
})
.unwrap();
}
}
68 changes: 54 additions & 14 deletions crates/lsp/src/supported_servers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ pub const TYPESCRIPT_LSP_REPAIR_ACTION: &str = "Install/repair TypeScript langua

const TYPESCRIPT_LSP_MISSING_BINARY_PREFIX: &str = "typescript-language-server is not available.";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LspStartupFailureReason {
MissingBinary { server_type: LSPServerType },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LspStartupError {
reason: LspStartupFailureReason,
message: String,
}

impl LspStartupError {
fn missing_binary(server_type: LSPServerType) -> Self {
Self {
reason: LspStartupFailureReason::MissingBinary { server_type },
message: actionable_missing_binary_message(server_type),
}
}

pub fn reason(&self) -> LspStartupFailureReason {
self.reason
}
}

impl std::fmt::Display for LspStartupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}

impl std::error::Error for LspStartupError {}

pub fn actionable_missing_binary_message(server_type: LSPServerType) -> String {
match server_type {
LSPServerType::TypeScriptLanguageServer => format!(
Expand All @@ -63,16 +95,6 @@ pub fn actionable_missing_binary_message(server_type: LSPServerType) -> String {
}
}

pub fn is_repairable_missing_binary_error(server_type: LSPServerType, error: &str) -> bool {
match server_type {
LSPServerType::TypeScriptLanguageServer => {
error.contains(TYPESCRIPT_LSP_MISSING_BINARY_PREFIX)
&& error.contains(TYPESCRIPT_LSP_REPAIR_ACTION)
}
_ => false,
}
}

#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn resolve_lsp_binary_config(
server_type: LSPServerType,
Expand Down Expand Up @@ -116,7 +138,7 @@ pub(crate) fn resolve_lsp_binary_config(
});
}

anyhow::bail!(actionable_missing_binary_message(server_type));
Err(LspStartupError::missing_binary(server_type).into())
}

/// Represents the different types of LSP servers supported by Warp.
Expand Down Expand Up @@ -406,10 +428,28 @@ mod tests {
assert!(error.contains("Install/repair TypeScript language server"));
assert!(error.contains("node_modules/.bin/typescript-language-server"));
assert!(error.contains("global install is not required"));
assert!(is_repairable_missing_binary_error(
}

#[test]
fn missing_binary_error_carries_typed_reason() {
let error = resolve_lsp_binary_config(
LSPServerType::TypeScriptLanguageServer,
&error
));
None,
None,
None,
false,
)
.unwrap_err();

let startup_error = error
.downcast_ref::<LspStartupError>()
.expect("missing binary should use typed LSP startup error");
assert_eq!(
startup_error.reason(),
LspStartupFailureReason::MissingBinary {
server_type: LSPServerType::TypeScriptLanguageServer,
}
);
}

#[test]
Expand Down