feat(lsp): 支持 ping 并宽容未知协议方法
健康检查在 initialize 前即可响应;未知请求记录 stderr 后返回 null,未知通知记录后忽略,避免插件因 MethodNotFound 判定服务端故障。
This commit is contained in:
1 parent
1b21ef14d5
commit
4aba50d60e
2 files changed
+101
-8
No files matched your search
@@ -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
|
||||
|
||||
+27
-3
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user