feat(lsp): Show configuration errors as "Not started", not "Crashed"

A `launch_error` (e.g. the chosen jdtls JDK is too old) never starts the
server, so reporting it as a crash was misleading. It is now its own
status, `Misconfigured` ("Not started"), with the reason as the message;
changing the settings retries as before. Run/Debug report the reason
instead of "jdtls is not ready (Misconfigured)".

Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
Цей коміт міститься в:
l0ng-ai committed 2026-10-24 18:16:06 +08:00
1 джерело f3d86d8ac7
коміт 7f59e00ad7
10 files changed
+37 -11

No files matched your search

+3
Переглянути файл
@@ -106,6 +106,9 @@ pub enum ServerStatus {
Crashed,
Stopped,
Missing,
/// Not started: its configuration can't launch it (e.g. no JDK new enough);
/// the message says what to change, and a settings change retries.
Misconfigured,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
+3
Переглянути файл
@@ -179,6 +179,9 @@ impl LspManager {
ide_core::event::ServerStatus::Missing => {
"jdtls is not installed (Settings › Languages & Frameworks › Language Servers)".to_owned()
}
ide_core::event::ServerStatus::Misconfigured => {
server.status_message().unwrap_or_else(|| "jdtls cannot start".to_owned())
}
_ => format!("jdtls is not ready ({status:?})"),
}))
}
+2
Переглянути файл
@@ -10,6 +10,8 @@ use std::sync::{Arc, OnceLock};
pub enum LaunchError {
/// The server binary is not installed / not on `PATH`.
Missing,
/// The configuration can't be launched (`LanguageServerConfig::launch_error`).
Config(String),
Failed(String),
}
+12 -3
Переглянути файл
@@ -339,7 +339,7 @@ impl LspManager {
if let Some(message) = &cfg.launch_error {
// A missing binary still asks to be installed first.
let missing = !me.launcher.available(&cfg, &root);
return Err(if missing { LaunchError::Missing } else { LaunchError::Failed(message.clone()) });
return Err(if missing { LaunchError::Missing } else { LaunchError::Config(message.clone()) });
}
me.launcher.launch(&cfg, &root, handler, Box::new(move |line| tail.push_stderr(line)))
})
@@ -358,6 +358,13 @@ impl LspManager {
}
return;
}
Err(LaunchError::Config(message)) => {
tracing::warn!("{}: {message}", server.name);
if server.finish_start(generation, Conn::Failed) {
server.set_status(&self.sink, ide_core::event::ServerStatus::Misconfigured, Some(message));
}
return;
}
Err(LaunchError::Failed(message)) => {
tracing::warn!("{}: {message}", server.name);
if server.finish_start(generation, Conn::Failed) {
@@ -1604,8 +1611,10 @@ impl LspManager {
s.reset_for_restart();
self.ensure_started(&s);
}
// Missing / crashed servers retry with the new configuration (e.g. a new PATH).
ide_core::event::ServerStatus::Missing | ide_core::event::ServerStatus::Crashed => {
// Missing / crashed / misconfigured servers retry with the new configuration (e.g. a new PATH).
ide_core::event::ServerStatus::Missing
| ide_core::event::ServerStatus::Crashed
| ide_core::event::ServerStatus::Misconfigured => {
s.reset_for_restart();
self.ensure_started(&s);
}
+6 -4
Переглянути файл
@@ -710,13 +710,15 @@ async fn launch_error_fails_without_starting_the_server() {
h.mgr.set_config_hook(Box::new(|cfg, _| cfg.launch_error = Some("needs JDK 21".into())));
assert!(!h.mgr.serves_path(&h.root.join("a.mk")), "batch inspection skips it");
h.open("a.mk", "x");
h.wait_status("mock", ServerStatus::Crashed).await;
// Not a crash: nothing ran, the configuration has to change.
h.wait_status("mock", ServerStatus::Misconfigured).await;
assert_eq!(h.launcher.state("mock").launches.load(Ordering::SeqCst), 0);
let crashed = h.events.lock().iter().find_map(|e| match e {
IdeEvent::LanguageServer { status: ServerStatus::Crashed, message, .. } => message.clone(),
let reason = h.events.lock().iter().find_map(|e| match e {
IdeEvent::LanguageServer { status: ServerStatus::Misconfigured, message, .. } => message.clone(),
_ => None,
});
assert_eq!(crashed.as_deref(), Some("needs JDK 21"));
assert_eq!(reason.as_deref(), Some("needs JDK 21"));
assert!(!h.statuses("mock").contains(&ServerStatus::Crashed));
}
#[tokio::test(flavor = "multi_thread")]
+1 -1
Переглянути файл
@@ -124,7 +124,7 @@ async fn rust_analyzer_end_to_end() {
mgr.did_open(doc, &main, &LanguageId::new("rust"), 1, MAIN_RS);
retry("rust-analyzer ready", Duration::from_secs(60), || {
let s = last_status(&events, "rust-analyzer");
if let Some((ServerStatus::Crashed | ServerStatus::Missing, msg)) = &s {
if let Some((ServerStatus::Crashed | ServerStatus::Missing | ServerStatus::Misconfigured, msg)) = &s {
panic!("rust-analyzer failed: {msg:?}");
}
async move { matches!(s, Some((ServerStatus::Ready | ServerStatus::Busy, _))).then_some(()) }
+6 -1
Переглянути файл
@@ -3397,7 +3397,12 @@ export type ServerInfo = {
pid: number | null,
};
export type ServerStatus = "starting" | "ready" | "busy" | "crashed" | "stopped" | "missing";
export type ServerStatus = "starting" | "ready" | "busy" | "crashed" | "stopped" | "missing" |
/**
* Not started: its configuration can't launch it (e.g. no JDK new enough);
* the message says what to change, and a settings change retries.
*/
"misconfigured";
/** Exception filters of one session (see [`DebugManager::exception_filters`]). */
export type SessionExceptionFilters = {
+1 -1
Переглянути файл
@@ -89,6 +89,6 @@ export function ServerStatus() {
function toneOf(status: string): "ok" | "busy" | "error" | "idle" {
if (status === "ready") return "ok";
if (status === "starting" || status === "busy") return "busy";
if (status === "crashed" || status === "missing") return "error";
if (status === "crashed" || status === "missing" || status === "misconfigured") return "error";
return "idle";
}
+2 -1
Переглянути файл
@@ -126,12 +126,13 @@ export const STATUS_LABEL: Record<ServerStatus, string> = {
crashed: "Crashed",
stopped: "Stopped",
missing: "Not installed",
misconfigured: "Not started",
};
/** One-line summary for the status bar. */
export function serversSummary(servers: readonly ServerEntry[]): { text: string; tone: "ok" | "busy" | "error" | "idle" } | null {
if (!servers.length) return null;
const bad = servers.filter((s) => s.status === "crashed" || s.status === "missing");
const bad = servers.filter((s) => s.status === "crashed" || s.status === "missing" || s.status === "misconfigured");
const busy = servers.filter((s) => s.status === "starting" || s.status === "busy");
const tone = bad.length ? "error" : busy.length ? "busy" : servers.some((s) => s.status === "ready") ? "ok" : "idle";
const names = [...new Set(servers.map((s) => s.name))];
+1
Переглянути файл
@@ -44,6 +44,7 @@ describe("serversSummary", () => {
expect(serversSummary([s("rust-analyzer", "ready")])).toEqual({ text: "rust-analyzer", tone: "ok" });
expect(serversSummary([s("vtsls", "ready"), s("eslint", "starting")])).toEqual({ text: "vtsls +1", tone: "busy" });
expect(serversSummary([s("gopls", "missing")])!.tone).toBe("error");
expect(serversSummary([s("jdtls", "misconfigured")])!.tone).toBe("error");
});
});