mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(lsp): Name the running server when it lacks a feature (#201)
ide-lsp returned the same `unavailable` error whether no server was running or a running server didn't implement the request, so the UI said no language server was available, and Invoke Inline Completion said no language server provides inline completion, while jdtls was running fine (it doesn't implement textDocument/inlineCompletion). When servers are running but none supports the feature, the error now names them ("jdtls doesn't support inline completion"), and every explicit action shares one message helper: Invoke Inline Completion, go to declaration / implementation / type declaration, quick documentation, parameter info, rename, reformat, intention actions, highlight usages, call / type hierarchy, find usages, safe delete and workspace symbols. With no server running it still says no language server is available. Background requests while typing stay silent.
Fixes #186
Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
This commit is contained in:
@@ -218,8 +218,17 @@ fn user_did_open(server: &Server, run: &Running, path: &Path, uri: &lsp::Uri, pa
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported(feature: &str) -> IdeError {
|
||||
IdeError::unavailable(format!("no running language server supports {feature}"))
|
||||
/// No target for `feature`: names the running servers that lack it, so the UI can
|
||||
/// tell "the server can't do this" from "no server is running" (the UI's
|
||||
/// `unavailableText` matches the "… doesn't support …" wording).
|
||||
fn unsupported(servers: &[Arc<Server>], feature: &str) -> IdeError {
|
||||
let running: Vec<&str> = servers.iter().filter(|s| s.running().is_some()).map(|s| s.name.as_str()).collect();
|
||||
if running.is_empty() {
|
||||
IdeError::unavailable(format!("no running language server supports {feature}"))
|
||||
} else {
|
||||
let servers = running.join(", ");
|
||||
IdeError::unavailable(format!("{servers} doesn't support {feature}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Folds per-server results: any success wins over errors; errors surface only
|
||||
@@ -830,7 +839,7 @@ impl LspManager {
|
||||
let snap = self.snapshot(doc)?;
|
||||
let targets = self.ready_targets(&snap.servers, pred).await;
|
||||
if targets.is_empty() {
|
||||
return Err(unsupported(feature));
|
||||
return Err(unsupported(&snap.servers, feature));
|
||||
}
|
||||
Ok((snap, targets))
|
||||
}
|
||||
@@ -1183,7 +1192,7 @@ impl LspManager {
|
||||
let servers: Vec<Arc<Server>> = self.servers.lock().clone();
|
||||
let targets = self.ready_targets(&servers, |c| c.has("workspaceSymbolProvider")).await;
|
||||
if targets.is_empty() {
|
||||
return Err(unsupported("workspace symbols"));
|
||||
return Err(unsupported(&servers, "workspace symbols"));
|
||||
}
|
||||
let requests = targets.iter().map(|t| {
|
||||
let cancel = &cancel;
|
||||
|
||||
@@ -520,6 +520,20 @@ async fn inline_completion_only_from_servers_that_advertise_it() {
|
||||
assert!(plain.requested("textDocument/inlineCompletion").is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn unsupported_feature_names_the_running_servers_that_lack_it() {
|
||||
let h = harness(&[("plain", incremental_caps()), ("other", json!({ "textDocumentSync": 2 }))]);
|
||||
let (doc, _) = h.open("a.mk", "let x = fo");
|
||||
for name in ["plain", "other"] {
|
||||
h.wait_status(name, ServerStatus::Ready).await;
|
||||
}
|
||||
let e = h.mgr.inline_completion(doc, Pos::new(0, 10), false, Default::default()).await.unwrap_err();
|
||||
assert_eq!(e.kind, ide_core::error::ErrorKind::Unavailable);
|
||||
assert_eq!(e.message, "plain, other doesn't support inline completion");
|
||||
let e = h.mgr.type_hierarchy(doc, Pos::new(0, 4), HierarchyDirection::Incoming).await.unwrap_err();
|
||||
assert_eq!(e.message, "plain, other doesn't support type hierarchy");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn diagnostics_become_events_and_feed_code_actions() {
|
||||
let h = harness(&[("mock", incremental_caps())]);
|
||||
|
||||
@@ -21,6 +21,7 @@ const {
|
||||
ghostText,
|
||||
inlineCompletionExtension,
|
||||
nextWord,
|
||||
registerInlineCompletionActions,
|
||||
requestInline,
|
||||
toSuggestions,
|
||||
} = await import("./inlineCompletion");
|
||||
@@ -136,3 +137,49 @@ describe("inline completion", () => {
|
||||
expect(nextWord(" ")).toBe(" ");
|
||||
});
|
||||
});
|
||||
|
||||
describe("inline completion without a provider", () => {
|
||||
const unavailable = (message: string) => ({
|
||||
status: "error" as const,
|
||||
error: { kind: "unavailable" as const, message },
|
||||
});
|
||||
const hintText = () => document.querySelector(".cm-lsp-hint")?.textContent ?? null;
|
||||
|
||||
async function invoke(view: EditorView) {
|
||||
const { useEditorRegistry } = await import("@/editor/api");
|
||||
const { getAction } = await import("@/platform/actions");
|
||||
useEditorRegistry.setState({ active: { view, ctx: view.state.facet(lspDoc)! } as never });
|
||||
const off = registerInlineCompletionActions();
|
||||
void getAction("editor.inlineCompletion.trigger")!.run({ ctx: {} } as never);
|
||||
off();
|
||||
}
|
||||
|
||||
it.each([
|
||||
[
|
||||
"names the running server that lacks it",
|
||||
"jdtls doesn't support inline completion",
|
||||
"jdtls doesn't support inline completion",
|
||||
],
|
||||
[
|
||||
"says no server runs when none does",
|
||||
"no running language server supports inline completion",
|
||||
"No language server available",
|
||||
],
|
||||
])("on explicit invocation %s", async (_, message, shown) => {
|
||||
const view = editor("String s = ");
|
||||
ipc.lspInlineCompletion.mockResolvedValueOnce(unavailable(message));
|
||||
await invoke(view);
|
||||
await vi.waitFor(() => expect(hintText()).toBe(shown));
|
||||
});
|
||||
|
||||
it("stays silent while typing", async () => {
|
||||
const view = editor("String s = ");
|
||||
ipc.lspInlineCompletion.mockResolvedValue(unavailable("jdtls doesn't support inline completion"));
|
||||
expect(await requestInline(view, true)).toBe(false);
|
||||
view.dispatch({ changes: { from: 11, insert: "x" }, userEvent: "input.type" });
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
expect(hintText()).toBeNull();
|
||||
expect(ghostDom(view)).toBeNull();
|
||||
ipc.lspInlineCompletion.mockReset();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type Command,
|
||||
type InlineCompletionItem,
|
||||
type RequestId,
|
||||
unavailableText,
|
||||
} from "@/platform/ipc";
|
||||
import { activeEditor } from "@/editor/api";
|
||||
import { docOf, isUnavailable, lspRequest } from "./lsp";
|
||||
@@ -312,7 +313,7 @@ export function registerInlineCompletionActions(): () => void {
|
||||
try {
|
||||
if (!(await requestInline(v, false))) showHint(v, "No inline suggestions");
|
||||
} catch (e) {
|
||||
showHint(v, isUnavailable(e) ? "No language server provides inline completion" : String(e));
|
||||
showHint(v, isUnavailable(e) ? unavailableText(e) : String(e));
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -8,7 +8,8 @@ vi.mock("@/platform/ipc", async (orig) => {
|
||||
return { ...actual, isTauri: () => true };
|
||||
});
|
||||
|
||||
const { lspRequest } = await import("./lsp");
|
||||
const { lspRequest, reportLspError } = await import("./lsp");
|
||||
const { IpcError } = await import("@/platform/ipc");
|
||||
|
||||
describe("lspRequest", () => {
|
||||
it("waits until open editors' edits reached the backend", async () => {
|
||||
@@ -27,3 +28,27 @@ describe("lspRequest", () => {
|
||||
view.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reportLspError", () => {
|
||||
const unavailable = (message: string) => new IpcError({ kind: "unavailable", message });
|
||||
const hintText = () => document.querySelector(".cm-lsp-hint")?.textContent ?? null;
|
||||
|
||||
it.each([
|
||||
["jdtls doesn't support type hierarchy", "jdtls doesn't support type hierarchy"],
|
||||
["pyright, ruff doesn't support code lens", "pyright, ruff doesn't support code lens"],
|
||||
["no running language server supports rename", "No language server available"],
|
||||
["jdtls is not running", "No language server available"],
|
||||
])("explicit %s → %s", (message, shown) => {
|
||||
const view = new EditorView({ state: EditorState.create({ doc: "x" }), parent: document.body });
|
||||
reportLspError(view, unavailable(message), true);
|
||||
expect(hintText()).toBe(shown);
|
||||
view.destroy();
|
||||
});
|
||||
|
||||
it("stays silent for background requests", () => {
|
||||
const view = new EditorView({ state: EditorState.create({ doc: "x" }), parent: document.body });
|
||||
reportLspError(view, unavailable("jdtls doesn't support inlay hints"), false);
|
||||
expect(hintText()).toBeNull();
|
||||
view.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Facet } from "@codemirror/state";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { whenAllSynced, type EditorDocContext } from "@/editor/api";
|
||||
import { IpcError, call, isCancelled, isTauri } from "@/platform/ipc";
|
||||
import { IpcError, call, isCancelled, isTauri, unavailableText } from "@/platform/ipc";
|
||||
import type { IdeError } from "@/bindings";
|
||||
import { showHint } from "./hint";
|
||||
|
||||
@@ -44,7 +44,8 @@ export const NO_SERVER = "No language server available";
|
||||
|
||||
/**
|
||||
* Error policy: background requests fail silently; explicit actions show a
|
||||
* small hint at the caret ("No language server available" for missing servers).
|
||||
* small hint at the caret ("No language server available" for missing servers,
|
||||
* "jdtls doesn't support X" when the running server lacks the feature).
|
||||
*/
|
||||
export function reportLspError(view: EditorView | null, e: unknown, explicit: boolean) {
|
||||
if (isCancelled(e)) return;
|
||||
@@ -52,7 +53,7 @@ export function reportLspError(view: EditorView | null, e: unknown, explicit: bo
|
||||
if (!isUnavailable(e)) console.debug("[lsp]", e);
|
||||
return;
|
||||
}
|
||||
showHint(view, isUnavailable(e) ? NO_SERVER : e instanceof Error ? e.message : String(e));
|
||||
showHint(view, isUnavailable(e) ? unavailableText(e, NO_SERVER) : e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
|
||||
/** Runs `fn` once `ms` after the last call; `cancel()` drops a pending run. */
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import type { Text } from "@codemirror/state";
|
||||
import type { LocalSymbol, Location, OutlineItem, Pos, Range, SyntaxNode } from "@/bindings";
|
||||
import { call, commands, isCancelled, isTauri, nextRequestId } from "@/platform/ipc";
|
||||
import { call, commands, isCancelled, isTauri, nextRequestId, unavailableText } from "@/platform/ipc";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { basename } from "@/platform/project";
|
||||
import { editorsFor, whenSynced, type EditorDocContext } from "@/editor/api";
|
||||
@@ -482,7 +482,12 @@ export async function safeDelete(view: EditorView): Promise<void> {
|
||||
applyDeletion(view, docBefore, target, comments);
|
||||
} catch (e) {
|
||||
if (isCancelled(e)) return;
|
||||
showHint(view, isUnavailable(e) ? cannotPerform("No language server is available") : cannotPerform(errorText(e)));
|
||||
showHint(
|
||||
view,
|
||||
isUnavailable(e)
|
||||
? cannotPerform(unavailableText(e, "No language server is available"))
|
||||
: cannotPerform(errorText(e)),
|
||||
);
|
||||
} finally {
|
||||
running = null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { EditorView } from "@codemirror/view";
|
||||
import { activeEditor, editorsFor } from "@/editor/api";
|
||||
import { isVirtualUri, virtualDocument } from "@/platform/virtualDocs";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { IpcError, call, commands, isCancelled, isTauri } from "@/platform/ipc";
|
||||
import { IpcError, call, commands, isCancelled, isTauri, unavailableText } from "@/platform/ipc";
|
||||
import type { IdeError } from "@/bindings";
|
||||
import { maybeEditorService, notifications } from "@/workbench/contributions";
|
||||
|
||||
@@ -36,7 +36,7 @@ export function hint(text: string, tone: "info" | "warning" | "error" = "info")
|
||||
export function reportError(e: unknown) {
|
||||
if (isCancelled(e)) return;
|
||||
hint(
|
||||
isUnavailable(e) ? "No language server available" : e instanceof Error ? e.message : String(e),
|
||||
isUnavailable(e) ? unavailableText(e) : e instanceof Error ? e.message : String(e),
|
||||
isUnavailable(e) ? "info" : "error",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,18 @@ export function isCancelled(e: unknown): boolean {
|
||||
return e instanceof IpcError && e.kind === "cancelled";
|
||||
}
|
||||
|
||||
/** Backend `unavailable` message when servers run but none has the feature: "jdtls doesn't support inline completion". */
|
||||
const LACKS_FEATURE = /^\S.* doesn't support \S.*$/;
|
||||
|
||||
/**
|
||||
* What to tell the user when a language-server feature is `unavailable`: the
|
||||
* running server that lacks it, so a working server doesn't read as missing;
|
||||
* `fallback` when no server is running.
|
||||
*/
|
||||
export function unavailableText(e: unknown, fallback = "No language server available"): string {
|
||||
return e instanceof IpcError && e.kind === "unavailable" && LACKS_FEATURE.test(e.message) ? e.message : fallback;
|
||||
}
|
||||
|
||||
// Ids are shared by every window of the backend and outlive a reload there (a cancel that
|
||||
// arrives before its request began is remembered), so each page starts at a random point.
|
||||
let nextId = 1 + Math.floor(Math.random() * 0x7000_0000);
|
||||
|
||||
@@ -2575,7 +2575,27 @@
|
||||
"Call Inline Completion": "调用内联补全",
|
||||
"Inline completion is off for this language": "此语言已关闭内联补全",
|
||||
"No inline suggestions": "没有内联建议",
|
||||
"No language server provides inline completion": "没有语言服务器提供内联补全",
|
||||
"{servers} doesn't support {feature}": "{servers} 不支持{feature}",
|
||||
"completion": "代码补全",
|
||||
"inline completion": "内联补全",
|
||||
"hover": "快速文档",
|
||||
"signature help": "形参信息",
|
||||
"go to definition": "转到定义",
|
||||
"go to declaration": "转到声明",
|
||||
"go to type definition": "转到类型声明",
|
||||
"go to implementation": "转到实现",
|
||||
"find references": "查找用法",
|
||||
"document highlights": "高亮用法",
|
||||
"code actions": "代码操作",
|
||||
"rename": "重命名",
|
||||
"formatting": "重新格式化代码",
|
||||
"range formatting": "格式化选中代码",
|
||||
"inlay hints": "嵌入提示",
|
||||
"code lens": "代码透镜",
|
||||
"document links": "文档链接",
|
||||
"call hierarchy": "调用层次结构",
|
||||
"type hierarchy": "类型层次结构",
|
||||
"workspace symbols": "工作区符号",
|
||||
"Insert Inline Proposal": "插入内联建议",
|
||||
"Insert Inline Proposal: Next Word": "插入内联建议:下一个单词",
|
||||
"Hide Inline Proposal": "隐藏内联建议",
|
||||
|
||||
Reference in New Issue
Block a user