From 4aba50d60e319aa6213bb245d0ff3f5e300b6520 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 26 Oct 2026 16:31:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(lsp):=20=E6=94=AF=E6=8C=81=20ping=20?= =?UTF-8?q?=E5=B9=B6=E5=AE=BD=E5=AE=B9=E6=9C=AA=E7=9F=A5=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 健康检查在 initialize 前即可响应;未知请求记录 stderr 后返回 null,未知通知记录后忽略,避免插件因 MethodNotFound 判定服务端故障。 --- App/lib/lsp/LspServer.vp | 79 +++++++++++++++++++++++++++++++++++++--- Tools/lsp/probe.py | 30 +++++++++++++-- 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/App/lib/lsp/LspServer.vp b/App/lib/lsp/LspServer.vp index b090b56..905292c 100644 --- a/App/lib/lsp/LspServer.vp +++ b/App/lib/lsp/LspServer.vp @@ -247,6 +247,59 @@ def _HandlePosition(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, return LspRpc.WriteResponse(pool, tr, id_node, result) +def _IsPingMethod(method: str) -> int: + """Health checks used by plugins; answered before initialize so a probe can never block startup.""" + if method is None: + return 0 + if string.strcmp(method, "$/ping") == 0: + return 1 + if string.strcmp(method, "ping") == 0: + return 1 + if string.strcmp(method, "viper/ping") == 0: + return 1 + if string.strcmp(method, "viper/pingCompiler") == 0: + return 1 + if string.strcmp(method, "$/health") == 0: + return 1 + if string.strcmp(method, "$/healthcheck") == 0: + return 1 + if string.strcmp(method, "health") == 0: + return 1 + return 0 + + +def _IsQuietNotification(method: str) -> int: + """Client notifications that this server accepts without action and without logging.""" + if method is None: + return 0 + if string.strcmp(method, "$/cancelRequest") == 0: + return 1 + if string.strcmp(method, "$/setTrace") == 0: + return 1 + if string.strcmp(method, "window/workDoneProgress/cancel") == 0: + return 1 + if string.strcmp(method, "workspace/didChangeConfiguration") == 0: + return 1 + if string.strcmp(method, "workspace/didChangeWatchedFiles") == 0: + return 1 + if string.strcmp(method, "textDocument/willSave") == 0: + return 1 + return 0 + + +def _WritePingResult(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, + id_node: json.JsonValue*) -> int: + if id_node is None: + return 0 + result: json.JsonValue* = json.object(pool) + if result is None: + return -1 + json.object_set(pool, result, "status", json.string_val(pool, "ok")) + json.object_set(pool, result, "server", json.string_val(pool, LSP_SERVER_NAME)) + json.object_set(pool, result, "version", json.string_val(pool, LSP_SERVER_VERSION)) + return LspRpc.WriteResponse(pool, tr, id_node, result) + + def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int: os._mbuddy = perm_pool os._win32._mbuddy = perm_pool @@ -301,7 +354,9 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int: params: json.JsonValue* = msg["params"] rc: int = 0 - if string.strcmp(method, LspProtocol.LSP_METHOD_INITIALIZE) == 0: + if _IsPingMethod(method) != 0: + rc = _WritePingResult(pool, tr, id_node) + elif string.strcmp(method, LspProtocol.LSP_METHOD_INITIALIZE) == 0: rc = _HandleInitialize(pool, tr, ws, id_node, params) elif string.strcmp(method, LspProtocol.LSP_METHOD_INITIALIZED) == 0: pass @@ -322,6 +377,12 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int: rc = LspNavigation.WorkspaceSymbol(pool, tr, docs, ws, id_node, params) elif string.strcmp(method, "textDocument/definition") == 0: rc = LspNavigation.Definition(pool, tr, docs, ws, id_node, params) + elif string.strcmp(method, "textDocument/declaration") == 0: + rc = LspNavigation.Definition(pool, tr, docs, ws, id_node, params) + elif string.strcmp(method, "textDocument/typeDefinition") == 0: + rc = LspNavigation.Definition(pool, tr, docs, ws, id_node, params) + elif string.strcmp(method, "textDocument/implementation") == 0: + rc = LspNavigation.Definition(pool, tr, docs, ws, id_node, params) elif string.strcmp(method, "textDocument/references") == 0: rc = LspNavigation.References(pool, tr, docs, ws, id_node, params) elif string.strcmp(method, "textDocument/documentHighlight") == 0: @@ -339,11 +400,19 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int: rc = LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) elif string.strcmp(method, LspProtocol.LSP_METHOD_EXIT) == 0: return 0 if shutdown_requested != 0 else 1 + elif _IsQuietNotification(method) != 0: + pass + elif id_node is not None: + # LSP clients treat MethodNotFound as a broken server. Unsupported features reply with a + # valid empty result and are reported on stderr instead, so the editor keeps working. + LspTransport.StderrWrite("viperc-lsp: unsupported request '") + LspTransport.StderrWrite(method) + LspTransport.StderrWrite("' (answered with null)\n") + rc = LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) else: - if id_node is not None: - rc = LspRpc.WriteError(pool, tr, id_node, - LspProtocol.LSP_ERROR_METHOD_NOT_FOUND, - "Method not found") + LspTransport.StderrWrite("viperc-lsp: ignored notification '") + LspTransport.StderrWrite(method) + LspTransport.StderrWrite("'\n") if rc != 0: return 1 diff --git a/Tools/lsp/probe.py b/Tools/lsp/probe.py index ef7da67..50e7a38 100644 --- a/Tools/lsp/probe.py +++ b/Tools/lsp/probe.py @@ -121,6 +121,15 @@ class Client: def lifecycle(exe: str) -> int: client = Client(exe) + # A health probe must answer before initialize; plugins use it to confirm the server is alive. + for method in ("$/ping", "viper/ping", "ping"): + ping = client.request(method, {}) + ping_result = ping.get("result") + if not isinstance(ping_result, dict) or ping_result.get("status") != "ok": + raise AssertionError(f"{method} did not return an ok result: {ping}") + if ping_result.get("server") != "viperc-lsp": + raise AssertionError(f"{method} returned the wrong server name: {ping}") + response = client.request("initialize", {"processId": os.getpid(), "capabilities": {}}) result = response.get("result") if not isinstance(result, dict): @@ -134,10 +143,24 @@ def lifecycle(exe: str) -> int: client.notify("initialized", {}) + # Unsupported features must not surface as protocol errors to the client. unknown = client.request("unknown/method", {}) - error = unknown.get("error") - if not isinstance(error, dict) or error.get("code") != -32601: - raise AssertionError(f"unknown method did not return MethodNotFound: {unknown}") + if "error" in unknown: + raise AssertionError(f"unknown method returned an error: {unknown}") + if "result" not in unknown or unknown.get("result") is not None: + raise AssertionError(f"unknown method did not return a null result: {unknown}") + + unsupported = client.request( + "textDocument/completion", + {"textDocument": {"uri": "file:///unsupported.vp"}, "position": {"line": 0, "character": 0}}, + ) + if "error" in unsupported or unsupported.get("result") is not None: + raise AssertionError(f"unsupported feature should answer null: {unsupported}") + + client.notify("unknown/notification", {}) + ping = client.request("$/ping", {}) + if ping.get("result", {}).get("status") != "ok": + raise AssertionError(f"server stopped answering after an unknown notification: {ping}") shutdown = client.request("shutdown") if shutdown.get("result") is not None: @@ -147,6 +170,7 @@ def lifecycle(exe: str) -> int: if code != 0: raise AssertionError(f"exit code is {code}, expected 0") print("lifecycle: OK") + print("ping: OK") return 0