Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
116 changes: 116 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,117 @@ 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;
use warpui::r#async::executor::Background;

fn install_crypto_provider() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
Comment thread
BunsDev marked this conversation as resolved.
Outdated
}

#[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 = std::env::var("CAST_CODES_DATA_PROFILE")
.expect("CAST_CODES_DATA_PROFILE is required for managed install smoke");
assert!(
data_profile.contains("ts-lsp-smoke"),
"managed install smoke must use an isolated data profile"
);

let workspace_root = PathBuf::from(
std::env::var("CASTCODES_TS_LSP_MISSING_REPO")
.expect("CASTCODES_TS_LSP_MISSING_REPO is required"),
);
Comment thread
BunsDev marked this conversation as resolved.
Outdated
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