1187 lines
38 KiB
Markdown
1187 lines
38 KiB
Markdown
# LSP Core Lifecycle Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 将 LSP core 改为严格的生命周期状态机、可取消异步请求、规范 JSON-RPC 错误和不可恢复 framing,并移除旧 Provider/兜底路径。
|
||
|
||
**Architecture:** `LspServer` 独占生命周期和控制消息,Dispatcher 在构造时绑定 manager/scheduler,仅负责 Provider 路由。生产入口与测试入口分别注入 Provider 注册器;测试入口仍运行同一个 server 和 stdio framing,以确定性阻塞/异常 Provider 验证异步取消和错误响应。
|
||
|
||
**Tech Stack:** C++23 Modules、Glaze、Taskflow/AsyncExecutor、LSP 3.17、Tree-sitter、CMake/CTest、Python 3
|
||
|
||
---
|
||
|
||
## Plan Meta
|
||
|
||
- **Plan Group:** `lsp-core-lifecycle`
|
||
- **Parent Plan:** none
|
||
- **Verification Scope:** `tsl-server`、`test_provider`、`test_scheduler`、`test_core_server`、`test_cli_startup`、LSP JSON transport smoke
|
||
- **Verification Gate:** 目标全部构建成功;上述 CTest 全部通过;core 无生命周期 bool、取消空分支、`"{}"`/固定 JSON 兜底或已删除 Provider 引用
|
||
- **Executor:** `executing-plans`
|
||
- **Constraints:** `karpathy-guidelines`、`.agents`、`AGENT_RULES.md`、不保留兼容层、所有提交使用规范 emoji
|
||
- **Worktree Note:** 当前工作区已有用户改动;保留所有无关修改。`lsp-server/src/cli/launcher.cppm` 已有 namespace/args_parser 改动,提交时只暂存本 Plan 新增的 registrar 和 `Run()` 退出码 hunk。
|
||
|
||
### Task 1: 建立 core 生命周期所有权和可测试服务器入口
|
||
|
||
**Files:**
|
||
|
||
- Create: `lsp-server/test/test_provider/core_server_fixture.cppm`
|
||
- Create: `lsp-server/test/test_core_server.py`
|
||
- Modify: `lsp-server/src/core/dispatcher.cppm`
|
||
- Modify: `lsp-server/src/core/server.cppm`
|
||
- Modify: `lsp-server/src/cli/launcher.cppm`
|
||
- Modify: `lsp-server/src/provider/base/interface.cppm`
|
||
- Modify: `lsp-server/src/provider/initialize/initialize.cppm`
|
||
- Modify: `lsp-server/src/provider/manifest.cppm`
|
||
- Delete: `lsp-server/src/provider/shutdown/shutdown.cppm`
|
||
- Delete: `lsp-server/src/provider/exit/exit.cppm`
|
||
- Delete: `lsp-server/src/provider/cancel_request/cancel_request.cppm`
|
||
- Modify: `lsp-server/test/test_provider/test_main.cppm`
|
||
- Modify: `lsp-server/test/test_provider/provider_misc_test.cppm`
|
||
- Modify: `lsp-server/test/test_provider/provider_surface_test.cppm`
|
||
- Modify: `lsp-server/test/test_provider/json_provider_coverage_test.cppm`
|
||
- Modify: `lsp-server/test/test_provider/CMakeLists.txt`
|
||
- Modify: `lsp-server/test/CMakeLists.txt`
|
||
|
||
- [ ] **Step 1: 写生命周期 RED 测试和测试服务器模式**
|
||
|
||
在 `core_server_fixture.cppm` 定义只用于测试进程的初始化 Provider,并通过正式
|
||
`LspServer` 运行:
|
||
|
||
```cpp
|
||
export module lsp.test.provider.core_server_fixture;
|
||
|
||
import std;
|
||
import spdlog;
|
||
|
||
import lsp.codec.facade;
|
||
import lsp.core.server;
|
||
import lsp.provider.base.interface;
|
||
import lsp.protocol;
|
||
|
||
export namespace lsp::test::provider
|
||
{
|
||
int RunCoreServerFixture();
|
||
}
|
||
|
||
namespace lsp::test::provider
|
||
{
|
||
class FixtureInitialize final : public core::IRequestProvider
|
||
{
|
||
public:
|
||
std::string GetMethod() const override { return "initialize"; }
|
||
std::string GetProviderName() const override { return "FixtureInitialize"; }
|
||
|
||
std::string ProvideResponse(const protocol::RequestMessage& request,
|
||
core::ExecutionContext&) override
|
||
{
|
||
protocol::ResponseMessage response;
|
||
response.id = request.id;
|
||
response.result = protocol::LSPAny(protocol::LSPObject{});
|
||
return codec::Serialize(response).value();
|
||
}
|
||
};
|
||
|
||
int RunCoreServerFixture()
|
||
{
|
||
spdlog::set_level(spdlog::level::off);
|
||
core::LspServer server(
|
||
std::cin,
|
||
std::cout,
|
||
[](core::RequestDispatcher& dispatcher) {
|
||
dispatcher.RegisterRequestProvider(
|
||
std::make_shared<FixtureInitialize>());
|
||
},
|
||
2,
|
||
"");
|
||
return server.Run();
|
||
}
|
||
}
|
||
```
|
||
|
||
在 `test_main.cppm` 的正常测试输出前处理该模式:
|
||
|
||
```cpp
|
||
if (arg == "--core-server-fixture")
|
||
return lsp::test::provider::RunCoreServerFixture();
|
||
```
|
||
|
||
在 `test_core_server.py` 添加 framing 工具和生命周期用例:
|
||
|
||
```python
|
||
def frame(message: dict) -> bytes:
|
||
body = json.dumps(message, separators=(",", ":")).encode("utf-8")
|
||
return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body
|
||
|
||
|
||
def read_messages(data: bytes) -> list[dict]:
|
||
messages = []
|
||
offset = 0
|
||
while offset < len(data):
|
||
header_end = data.find(b"\r\n\r\n", offset)
|
||
assert header_end >= 0
|
||
header = data[offset:header_end].decode("ascii")
|
||
fields = dict(line.split(": ", 1) for line in header.split("\r\n"))
|
||
length = int(fields["Content-Length"])
|
||
body_start = header_end + 4
|
||
body_end = body_start + length
|
||
assert body_end <= len(data)
|
||
messages.append(json.loads(data[body_start:body_end].decode("utf-8")))
|
||
offset = body_end
|
||
return messages
|
||
|
||
|
||
def response_by_id(data: bytes, request_id: int | str) -> dict:
|
||
return next(message for message in read_messages(data)
|
||
if message.get("id") == request_id)
|
||
|
||
|
||
def run_raw(server: Path, payload: bytes) -> subprocess.CompletedProcess[bytes]:
|
||
return subprocess.run(
|
||
[str(server), "--core-server-fixture"],
|
||
input=payload,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
timeout=5,
|
||
check=False,
|
||
)
|
||
|
||
|
||
def run_batch(server: Path, messages: list[dict]) -> subprocess.CompletedProcess[bytes]:
|
||
payload = b"".join(frame(message) for message in messages)
|
||
return run_raw(server, payload)
|
||
|
||
|
||
class LspClient:
|
||
def __init__(self, server: Path) -> None:
|
||
self.proc = subprocess.Popen(
|
||
[str(server), "--core-server-fixture"],
|
||
stdin=subprocess.PIPE,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
)
|
||
self.buffer = b""
|
||
|
||
def send(self, message: dict) -> None:
|
||
assert self.proc.stdin is not None
|
||
self.proc.stdin.write(frame(message))
|
||
self.proc.stdin.flush()
|
||
|
||
def read(self, timeout: float = 1.0) -> dict:
|
||
assert self.proc.stdout is not None
|
||
deadline = time.monotonic() + timeout
|
||
while True:
|
||
header_end = self.buffer.find(b"\r\n\r\n")
|
||
if header_end >= 0:
|
||
header = self.buffer[:header_end].decode("ascii")
|
||
fields = dict(line.split(": ", 1)
|
||
for line in header.split("\r\n"))
|
||
length = int(fields["Content-Length"])
|
||
body_start = header_end + 4
|
||
body_end = body_start + length
|
||
if len(self.buffer) >= body_end:
|
||
body = self.buffer[body_start:body_end]
|
||
self.buffer = self.buffer[body_end:]
|
||
return json.loads(body.decode("utf-8"))
|
||
|
||
remaining = deadline - time.monotonic()
|
||
if remaining <= 0:
|
||
raise TimeoutError("timed out waiting for LSP response")
|
||
ready, _, _ = select.select(
|
||
[self.proc.stdout.fileno()], [], [], remaining)
|
||
if not ready:
|
||
continue
|
||
chunk = os.read(self.proc.stdout.fileno(), 4096)
|
||
if not chunk:
|
||
raise RuntimeError("fixture closed stdout before a response")
|
||
self.buffer += chunk
|
||
|
||
def close_input(self) -> int:
|
||
assert self.proc.stdin is not None
|
||
self.proc.stdin.close()
|
||
return self.proc.wait(timeout=5)
|
||
|
||
|
||
def assert_lifecycle(server: Path) -> None:
|
||
before_init = run_batch(server, [
|
||
{"jsonrpc": "2.0", "id": 1, "method": "shutdown"},
|
||
{"jsonrpc": "2.0", "method": "exit"},
|
||
])
|
||
assert before_init.returncode == 1
|
||
assert response_by_id(before_init.stdout, 1)["error"]["code"] == -32002
|
||
|
||
repeated = run_batch(server, [
|
||
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}},
|
||
{"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {}},
|
||
{"jsonrpc": "2.0", "id": 3, "method": "shutdown"},
|
||
{"jsonrpc": "2.0", "method": "exit"},
|
||
])
|
||
assert repeated.returncode == 0
|
||
assert response_by_id(repeated.stdout, 2)["error"]["code"] == -32600
|
||
assert response_by_id(repeated.stdout, 3)["result"] is None
|
||
|
||
direct_exit = run_batch(
|
||
server, [{"jsonrpc": "2.0", "method": "exit"}])
|
||
assert direct_exit.returncode == 1
|
||
|
||
client = LspClient(server)
|
||
client.send({"jsonrpc": "2.0", "id": 11,
|
||
"method": "initialize", "params": {}})
|
||
assert client.read()["id"] == 11
|
||
client.send({"jsonrpc": "2.0", "id": 12, "method": "shutdown"})
|
||
assert client.read()["result"] is None
|
||
assert client.proc.poll() is None
|
||
client.send({"jsonrpc": "2.0", "method": "exit"})
|
||
assert client.close_input() == 0
|
||
```
|
||
|
||
脚本导入 `argparse`、`json`、`os`、`select`、`subprocess`、`time` 和 `Path`,
|
||
解析必填 `--server` 后调用 `assert_lifecycle(Path(args.server))` 并返回 0。响应
|
||
解析严格按 `Content-Length` 切分,不按换行猜测 JSON 边界。
|
||
|
||
在 `test_provider/CMakeLists.txt` 的 source 和 Module file-set 中加入 fixture、
|
||
`server.cppm`、`bridge/win32_stdio.cppm`、`manager/bootstrap.cppm`;在父级
|
||
`test/CMakeLists.txt` 的 Python 分支注册:
|
||
|
||
```cmake
|
||
add_test(NAME test_core_server
|
||
COMMAND ${PYTHON3_EXECUTABLE}
|
||
${CMAKE_CURRENT_LIST_DIR}/test_core_server.py
|
||
--server $<TARGET_FILE:test_provider>)
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试并确认 RED**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
|
||
```
|
||
|
||
Expected: FAIL,`LspServer` 还没有 stream/registrar 构造函数且 `Run()` 仍返回
|
||
`void`;失败来自新生命周期接口,而不是 Python 语法错误。
|
||
|
||
- [ ] **Step 3: 收紧 Dispatcher 构造与错误响应接口**
|
||
|
||
将 Dispatcher 改为构造即有效:
|
||
|
||
```cpp
|
||
class ExecutionContext
|
||
{
|
||
public:
|
||
ExecutionContext(scheduler::async_executor::AsyncExecutor& scheduler,
|
||
manager::ManagerHub& manager_hub)
|
||
: async_executor_(scheduler), manager_hub_(manager_hub) {}
|
||
|
||
scheduler::async_executor::AsyncExecutor& GetScheduler() const;
|
||
manager::ManagerHub& GetManagerHub() const;
|
||
|
||
private:
|
||
scheduler::async_executor::AsyncExecutor& async_executor_;
|
||
manager::ManagerHub& manager_hub_;
|
||
};
|
||
|
||
class RequestDispatcher
|
||
{
|
||
public:
|
||
RequestDispatcher(scheduler::async_executor::AsyncExecutor& scheduler,
|
||
manager::ManagerHub& manager_hub);
|
||
|
||
private:
|
||
scheduler::async_executor::AsyncExecutor& async_executor_;
|
||
manager::ManagerHub& manager_hub_;
|
||
};
|
||
```
|
||
|
||
删除 `ServerLifecycleEvent`、`LifecycleCallback`、两个 setter、callback 注册/通知
|
||
和所有 nullable dependency 分支。错误构造只保留一套实现:
|
||
|
||
```cpp
|
||
std::string BuildErrorResponseMessage(
|
||
std::optional<protocol::RequestId> id,
|
||
protocol::ErrorCodes code,
|
||
std::string_view message)
|
||
{
|
||
protocol::ResponseMessage response;
|
||
response.id = std::move(id);
|
||
response.error = protocol::ResponseError{
|
||
.jsonrpc = "2.0",
|
||
.code = static_cast<protocol::integer>(code),
|
||
.message = std::string(message),
|
||
.data = std::nullopt,
|
||
};
|
||
return transform::Serialize(response).value();
|
||
}
|
||
|
||
std::string BuildErrorResponseMessage(
|
||
const protocol::RequestMessage& request,
|
||
protocol::ErrorCodes code,
|
||
std::string_view message)
|
||
{
|
||
return BuildErrorResponseMessage(request.id, code, message);
|
||
}
|
||
```
|
||
|
||
`HandleUnknownRequest` 调用该函数;序列化失败直接抛出,不返回 `"{}"` 或固定
|
||
JSON。
|
||
|
||
- [ ] **Step 4: 实现 server 状态机和控制消息**
|
||
|
||
增加可注入注册器、stream 引用和显式状态:
|
||
|
||
```cpp
|
||
using ProviderRegistrar = std::function<void(RequestDispatcher&)>;
|
||
|
||
enum class ServerState
|
||
{
|
||
kUninitialized,
|
||
kRunning,
|
||
kShutdownRequested,
|
||
kExiting,
|
||
};
|
||
|
||
LspServer(std::istream& input,
|
||
std::ostream& output,
|
||
ProviderRegistrar registrar,
|
||
std::size_t concurrency,
|
||
std::string interpreter_path);
|
||
|
||
int Run();
|
||
```
|
||
|
||
成员按依赖顺序声明:
|
||
|
||
```cpp
|
||
std::istream& input_;
|
||
std::ostream& output_;
|
||
manager::ManagerHub manager_hub_;
|
||
scheduler::async_executor::AsyncExecutor async_executor_;
|
||
RequestDispatcher dispatcher_;
|
||
ServerState state_ = ServerState::kUninitialized;
|
||
int exit_code_ = 1;
|
||
std::atomic<bool> fatal_io_error_ = false;
|
||
```
|
||
|
||
请求状态策略集中在 `HandleRequest`:
|
||
|
||
```cpp
|
||
if (request.method == "initialize")
|
||
{
|
||
if (state_ != ServerState::kUninitialized)
|
||
return SendError(request, protocol::ErrorCodes::InvalidRequest,
|
||
"Server is already initialized");
|
||
|
||
const auto response = dispatcher_.Dispatch(request);
|
||
SendMessage(response);
|
||
const auto parsed = transform::Deserialize<protocol::ResponseMessage>(response);
|
||
if (!parsed || parsed->error)
|
||
return;
|
||
state_ = ServerState::kRunning;
|
||
return;
|
||
}
|
||
|
||
if (request.method == "shutdown")
|
||
{
|
||
if (state_ == ServerState::kUninitialized)
|
||
return SendError(request, protocol::ErrorCodes::ServerNotInitialized,
|
||
"Server not initialized");
|
||
if (state_ != ServerState::kRunning)
|
||
return SendError(request, protocol::ErrorCodes::InvalidRequest,
|
||
"Shutdown already requested");
|
||
|
||
async_executor_.WaitAll();
|
||
manager_hub_.Shutdown();
|
||
protocol::ResponseMessage response;
|
||
response.id = request.id;
|
||
response.result = protocol::LSPAny(std::nullptr_t{});
|
||
SendMessage(transform::Serialize(response).value());
|
||
state_ = ServerState::kShutdownRequested;
|
||
return;
|
||
}
|
||
```
|
||
|
||
生命周期控制分支之后,普通请求只允许在 `kRunning`:
|
||
|
||
```cpp
|
||
if (state_ == ServerState::kUninitialized)
|
||
{
|
||
SendError(request, protocol::ErrorCodes::ServerNotInitialized,
|
||
"Server not initialized");
|
||
return;
|
||
}
|
||
if (state_ == ServerState::kShutdownRequested)
|
||
{
|
||
SendError(request, protocol::ErrorCodes::InvalidRequest,
|
||
"Server is shutting down");
|
||
return;
|
||
}
|
||
SendMessage(dispatcher_.Dispatch(request));
|
||
```
|
||
|
||
notification 策略只有一个入口:`exit` 在任何状态都终止;`kUninitialized` 忽略
|
||
所有其他通知;`kShutdownRequested` 只允许 `exit`;`kRunning` 才 dispatch
|
||
普通通知。Task 3 再在普通通知之前插入 `$/cancelRequest` 控制分支。
|
||
|
||
`SendMessage` 使用注入的 output stream,并把写失败标为 fatal:
|
||
|
||
```cpp
|
||
void LspServer::SendMessage(const std::string& message)
|
||
{
|
||
std::lock_guard lock(output_mutex_);
|
||
output_ << "Content-Length: " << message.size() << "\r\n\r\n"
|
||
<< message << std::flush;
|
||
if (!output_)
|
||
{
|
||
fatal_io_error_ = true;
|
||
throw std::runtime_error("Failed to write LSP message");
|
||
}
|
||
}
|
||
```
|
||
|
||
主循环每次读取前检查 `fatal_io_error_` 并以 1 结束。同步写异常传播到 launcher;
|
||
异步 callback 写异常由 executor 记录,但 fatal flag 会在主循环下一控制点生效。
|
||
|
||
`exit` 不再 dispatch:保存旧状态,设置 `kExiting`,只有旧状态为
|
||
`kShutdownRequested` 时把 `exit_code_` 设为 0。EOF 保持 1。删除析构函数中的
|
||
状态写入、主循环 sleep、`RequiresSyncProcessing`、`CanProcessRequest` 和
|
||
生命周期事件入口。
|
||
|
||
- [ ] **Step 5: 删除 lifecycle Provider 和迁移调用点**
|
||
|
||
删除 shutdown/exit/cancel_request 三个 Module。`AllProviders` 移除 `Shutdown`;
|
||
`Initialize::ProvideResponse` 删除事件触发,只在序列化失败时抛出。Provider
|
||
bridge 只保留实际使用的接口别名:
|
||
|
||
```cpp
|
||
using ExecutionContext = lsp::core::ExecutionContext;
|
||
using IProvider = lsp::core::IProvider;
|
||
using IRequestProvider = lsp::core::IRequestProvider;
|
||
using INotificationProvider = lsp::core::INotificationProvider;
|
||
using lsp::core::BuildErrorResponseMessage;
|
||
```
|
||
|
||
测试环境统一改为:
|
||
|
||
```cpp
|
||
struct ProviderEnv
|
||
{
|
||
scheduler::async_executor::AsyncExecutor scheduler{1};
|
||
manager::ManagerHub hub{};
|
||
core::RequestDispatcher dispatcher{scheduler, hub};
|
||
core::ExecutionContext context{scheduler, hub};
|
||
};
|
||
```
|
||
|
||
删除 provider misc/surface 中三个已删除 Provider 的 import、声明、注册、子进程
|
||
入口和断言;Initialize 测试只验证响应与 manager 状态。同步移除 test CMake
|
||
中的三个 Module。
|
||
|
||
launcher 显式注入正式注册器并返回 server 退出码:
|
||
|
||
```cpp
|
||
core::LspServer server(
|
||
std::cin,
|
||
std::cout,
|
||
provider::RegisterAllProviders,
|
||
config.thread_count,
|
||
config.interpreter_path);
|
||
const int exit_code = server.Run();
|
||
spdlog::shutdown();
|
||
return exit_code;
|
||
```
|
||
|
||
- [ ] **Step 6: 运行 GREEN 验证**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure
|
||
```
|
||
|
||
Expected: build PASS;生命周期用例 PASS,shutdown 响应含 `result: null`,正常
|
||
关闭返回 0,异常退出返回 1。
|
||
|
||
- [ ] **Step 7: 提交生命周期改动**
|
||
|
||
只暂存本 Task 文件;launcher 使用逐 hunk 暂存,保留原有 namespace/args_parser
|
||
改动。检查后提交:
|
||
|
||
```bash
|
||
git diff --cached --check
|
||
git commit -m ":bug: fix(core): enforce strict LSP lifecycle"
|
||
```
|
||
|
||
### Task 2: 严格分类 JSON-RPC 消息并统一请求错误
|
||
|
||
**Files:**
|
||
|
||
- Modify: `lsp-server/test/test_core_server.py`
|
||
- Modify: `lsp-server/src/core/server.cppm`
|
||
- Modify: `lsp-server/src/core/dispatcher.cppm`
|
||
|
||
- [ ] **Step 1: 写 ParseError、InvalidRequest 和 MethodNotFound RED 测试**
|
||
|
||
新增批量原始消息测试:
|
||
|
||
```python
|
||
def raw_frame(body: bytes) -> bytes:
|
||
return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body
|
||
|
||
|
||
def assert_json_rpc_errors(server: Path) -> None:
|
||
payload = b"".join([
|
||
raw_frame(b'{"jsonrpc":"2.0",'),
|
||
raw_frame(b'[]'),
|
||
raw_frame(b'{"jsonrpc":"1.0","id":7,"method":"initialize"}'),
|
||
frame({"jsonrpc": "2.0", "id": 8, "method": "initialize", "params": {}}),
|
||
frame({"jsonrpc": "2.0", "id": 9, "method": "missing/method"}),
|
||
frame({"jsonrpc": "2.0", "id": 10, "method": "shutdown"}),
|
||
frame({"jsonrpc": "2.0", "method": "exit"}),
|
||
])
|
||
result = run_raw(server, payload)
|
||
assert result.returncode == 0
|
||
responses = read_messages(result.stdout)
|
||
assert responses[0]["error"]["code"] == -32700
|
||
assert responses[0]["id"] is None
|
||
assert responses[1]["error"]["code"] == -32600
|
||
assert response_by_id(result.stdout, 7)["error"]["code"] == -32600
|
||
assert response_by_id(result.stdout, 9)["error"]["code"] == -32601
|
||
```
|
||
|
||
把 `assert_json_rpc_errors(server)` 加入脚本 `main()`,确保 CTest 实际执行新用例。
|
||
|
||
- [ ] **Step 2: 运行测试并确认 RED**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure
|
||
```
|
||
|
||
Expected: FAIL,非法 JSON/结构当前只写日志,没有对应响应。
|
||
|
||
- [ ] **Step 3: 实现严格消息分类**
|
||
|
||
`HandleMessage` 按以下顺序处理:
|
||
|
||
```cpp
|
||
auto any = transform::Deserialize<protocol::LSPAny>(raw_message);
|
||
if (!any)
|
||
{
|
||
SendError(std::nullopt, protocol::ErrorCodes::ParseError, "Parse error");
|
||
return;
|
||
}
|
||
if (!any->Is<protocol::LSPObject>())
|
||
{
|
||
SendError(std::nullopt, protocol::ErrorCodes::InvalidRequest,
|
||
"Invalid Request");
|
||
return;
|
||
}
|
||
|
||
const auto& object = any->Get<protocol::LSPObject>();
|
||
const auto jsonrpc = object.find("jsonrpc");
|
||
if (jsonrpc == object.end() || !jsonrpc->second.Is<protocol::string>() ||
|
||
jsonrpc->second.Get<protocol::string>() != "2.0")
|
||
{
|
||
SendError(ExtractRequestId(object), protocol::ErrorCodes::InvalidRequest,
|
||
"Invalid Request");
|
||
return;
|
||
}
|
||
```
|
||
|
||
`ExtractRequestId` 只接受 integer/string:
|
||
|
||
```cpp
|
||
std::optional<protocol::RequestId> ExtractRequestId(
|
||
const protocol::LSPObject& object)
|
||
{
|
||
const auto id = object.find("id");
|
||
if (id == object.end())
|
||
return std::nullopt;
|
||
if (id->second.Is<protocol::integer>())
|
||
return protocol::RequestId{id->second.Get<protocol::integer>()};
|
||
if (id->second.Is<protocol::string>())
|
||
return protocol::RequestId{id->second.Get<protocol::string>()};
|
||
return std::nullopt;
|
||
}
|
||
```
|
||
|
||
method 必须是 string;有 method+id 是
|
||
request,无 id 是 notification;response 必须有 id 且 `result`/`error` 恰好一
|
||
个。不可分类的 response 只记录,不发送 response-to-response。
|
||
|
||
`SendError` 只负责调用公共 `BuildErrorResponseMessage` 后 `SendMessage`,不再
|
||
自己构造 `ResponseMessage`。
|
||
|
||
- [ ] **Step 4: 运行 GREEN 验证并提交**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure
|
||
```
|
||
|
||
Expected: PASS;错误码和 ID 与测试一致。
|
||
|
||
Commit:
|
||
|
||
```bash
|
||
git add lsp-server/src/core/server.cppm lsp-server/src/core/dispatcher.cppm lsp-server/test/test_core_server.py
|
||
git diff --cached --check
|
||
git commit -m ":bug: fix(core): return JSON-RPC protocol errors"
|
||
```
|
||
|
||
### Task 3: 异步执行普通请求并精确取消
|
||
|
||
**Files:**
|
||
|
||
- Modify: `lsp-server/test/test_provider/core_server_fixture.cppm`
|
||
- Modify: `lsp-server/test/test_core_server.py`
|
||
- Modify: `lsp-server/src/core/dispatcher.cppm`
|
||
- Modify: `lsp-server/src/core/server.cppm`
|
||
|
||
- [ ] **Step 1: 增加阻塞、异常 Provider 和取消 RED 测试**
|
||
|
||
Fixture 注册两个请求 Provider 和一个通知 Provider:
|
||
|
||
```cpp
|
||
class FixtureBlock final : public core::IRequestProvider
|
||
{
|
||
public:
|
||
std::string GetMethod() const override { return "test/block"; }
|
||
std::string GetProviderName() const override { return "FixtureBlock"; }
|
||
|
||
std::string ProvideResponse(const protocol::RequestMessage& request,
|
||
core::ExecutionContext& context) override
|
||
{
|
||
const auto deadline = std::chrono::steady_clock::now() +
|
||
std::chrono::seconds(2);
|
||
while (!context.GetStopToken().stop_requested() &&
|
||
std::chrono::steady_clock::now() < deadline)
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||
|
||
protocol::ResponseMessage response;
|
||
response.id = request.id;
|
||
response.result = protocol::LSPAny(protocol::string("completed"));
|
||
return codec::Serialize(response).value();
|
||
}
|
||
};
|
||
|
||
class FixtureThrow final : public core::IRequestProvider
|
||
{
|
||
public:
|
||
std::string GetMethod() const override { return "test/throw"; }
|
||
std::string GetProviderName() const override { return "FixtureThrow"; }
|
||
std::string ProvideResponse(const protocol::RequestMessage&,
|
||
core::ExecutionContext&) override
|
||
{
|
||
throw std::runtime_error("fixture request failure");
|
||
}
|
||
};
|
||
|
||
class FixtureThrowNotification final : public core::INotificationProvider
|
||
{
|
||
public:
|
||
std::string GetMethod() const override { return "test/throwNotification"; }
|
||
std::string GetProviderName() const override
|
||
{
|
||
return "FixtureThrowNotification";
|
||
}
|
||
void HandleNotification(const protocol::NotificationMessage&,
|
||
core::ExecutionContext&) override
|
||
{
|
||
throw std::runtime_error("fixture notification failure");
|
||
}
|
||
};
|
||
```
|
||
|
||
registrar 中加入:
|
||
|
||
```cpp
|
||
dispatcher.RegisterRequestProvider(std::make_shared<FixtureBlock>());
|
||
dispatcher.RegisterRequestProvider(std::make_shared<FixtureThrow>());
|
||
dispatcher.RegisterNotificationProvider(
|
||
std::make_shared<FixtureThrowNotification>());
|
||
```
|
||
|
||
Python 交互测试必须覆盖:
|
||
|
||
```python
|
||
client = LspClient(server)
|
||
client.send({"jsonrpc": "2.0", "id": 1,
|
||
"method": "initialize", "params": {}})
|
||
assert client.read()["id"] == 1
|
||
|
||
client.send({"jsonrpc": "2.0", "id": 2, "method": "test/block"})
|
||
client.send({"jsonrpc": "2.0", "id": "2", "method": "test/block"})
|
||
client.send({"jsonrpc": "2.0", "method": "$/cancelRequest",
|
||
"params": {"id": 2}})
|
||
cancelled = client.read(timeout=1)
|
||
assert cancelled["id"] == 2
|
||
assert cancelled["error"]["code"] == -32800
|
||
|
||
client.send({"jsonrpc": "2.0", "method": "$/cancelRequest",
|
||
"params": {"id": "2"}})
|
||
string_cancelled = client.read(timeout=1)
|
||
assert string_cancelled["id"] == "2"
|
||
assert string_cancelled["error"]["code"] == -32800
|
||
|
||
client.send({"jsonrpc": "2.0", "id": 3, "method": "test/throw"})
|
||
failed = client.read(timeout=1)
|
||
assert failed["error"]["code"] == -32603
|
||
|
||
client.send({"jsonrpc": "2.0", "id": 4, "method": "test/block"})
|
||
client.send({"jsonrpc": "2.0", "id": 4, "method": "test/block"})
|
||
duplicate = client.read(timeout=1)
|
||
assert duplicate["id"] == 4
|
||
assert duplicate["error"]["code"] == -32600
|
||
client.send({"jsonrpc": "2.0", "method": "$/cancelRequest",
|
||
"params": {"id": 4}})
|
||
cancelled_original = client.read(timeout=1)
|
||
assert cancelled_original["error"]["code"] == -32800
|
||
|
||
client.send({"jsonrpc": "2.0", "method": "test/throwNotification"})
|
||
client.send({"jsonrpc": "2.0", "id": 5, "method": "shutdown"})
|
||
assert client.read(timeout=1)["id"] == 5
|
||
client.send({"jsonrpc": "2.0", "method": "exit"})
|
||
assert client.close_input() == 0
|
||
```
|
||
|
||
把该交互过程封装为 `assert_cancellation_and_failures(server)` 并加入脚本
|
||
`main()`。
|
||
|
||
- [ ] **Step 2: 运行测试并确认 RED**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target test_provider -j1
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure
|
||
```
|
||
|
||
Expected: FAIL;`GetStopToken` 尚不存在,或同步 block 请求使取消响应超过 1 秒。
|
||
|
||
- [ ] **Step 3: 向 ExecutionContext 传递 stop token**
|
||
|
||
```cpp
|
||
ExecutionContext(scheduler::async_executor::AsyncExecutor& scheduler,
|
||
manager::ManagerHub& manager_hub,
|
||
std::stop_token stop_token = {})
|
||
: async_executor_(scheduler),
|
||
manager_hub_(manager_hub),
|
||
stop_token_(stop_token) {}
|
||
|
||
std::stop_token GetStopToken() const { return stop_token_; }
|
||
```
|
||
|
||
`RequestDispatcher::Dispatch` 改为接收默认值为空的 `std::stop_token` 并用它
|
||
构造 context;同步 initialize 和 notification 使用空 token。
|
||
|
||
- [ ] **Step 4: 实现类型安全请求表和异步 callback**
|
||
|
||
server 增加:
|
||
|
||
```cpp
|
||
struct ActiveRequest
|
||
{
|
||
scheduler::async_executor::TaskHandle handle;
|
||
};
|
||
|
||
std::mutex requests_mutex_;
|
||
std::unordered_map<std::string, std::shared_ptr<ActiveRequest>> active_requests_;
|
||
```
|
||
|
||
请求键必须带类型:
|
||
|
||
```cpp
|
||
std::string RequestKey(const protocol::RequestId& id)
|
||
{
|
||
return std::visit([](const auto& value) {
|
||
using T = std::decay_t<decltype(value)>;
|
||
if constexpr (std::is_same_v<T, protocol::integer>)
|
||
return "i:" + std::to_string(value);
|
||
else
|
||
return "s:" + value;
|
||
}, id);
|
||
}
|
||
```
|
||
|
||
`SubmitRequest` 先登记 shared state,解锁后 Submit,再绑定 handle:
|
||
|
||
```cpp
|
||
auto state = std::make_shared<ActiveRequest>();
|
||
bool duplicate = false;
|
||
{
|
||
std::lock_guard lock(requests_mutex_);
|
||
if (active_requests_.contains(key))
|
||
duplicate = true;
|
||
else
|
||
active_requests_.emplace(key, state);
|
||
}
|
||
if (duplicate)
|
||
{
|
||
SendError(request, protocol::ErrorCodes::InvalidRequest,
|
||
"Duplicate active request id");
|
||
return;
|
||
}
|
||
|
||
state->handle = async_executor_.Submit(
|
||
"lsp-request:" + key,
|
||
[this, request](std::stop_token stop_token) -> std::optional<std::string> {
|
||
return dispatcher_.Dispatch(request, stop_token);
|
||
},
|
||
[this, request, key, state](const auto& result) {
|
||
FinishRequest(request, key, state, result);
|
||
});
|
||
```
|
||
|
||
`FinishRequest` 先仅当映射仍指向同一 state 时删除登记并释放请求表锁,再对
|
||
completed/cancelled/failed 分别发送结果、`RequestCancelled`、`InternalError`。
|
||
这样输出异常不会留下陈旧登记,callback 也不会在请求表锁内发送消息。
|
||
|
||
`HandleCancelRequest` 严格读取 `CancelParams`,复制 handle 后解锁并调用
|
||
`Cancel()`。`DrainActiveRequests` 锁内复制 handles、锁外 cancel/wait;shutdown、
|
||
exit、EOF/framing 收尾都调用它。shutdown 随后调用
|
||
`async_executor_.WaitAll()`,保证 initialize/Provider 提交的后台任务也在
|
||
`ManagerHub::Shutdown()` 前结束。
|
||
|
||
- [ ] **Step 5: 捕获 notification 异常并运行 GREEN**
|
||
|
||
notification dispatch 外围只记录异常:
|
||
|
||
```cpp
|
||
try
|
||
{
|
||
dispatcher_.Dispatch(notification);
|
||
}
|
||
catch (const std::exception& error)
|
||
{
|
||
spdlog::error("Notification {} failed: {}",
|
||
notification.method, error.what());
|
||
}
|
||
catch (...)
|
||
{
|
||
spdlog::error("Notification {} failed with unknown exception",
|
||
notification.method);
|
||
}
|
||
```
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_scheduler|test_provider|test_core_server)$' --output-on-failure
|
||
```
|
||
|
||
Expected: PASS;两种 ID 分别取消,异常请求返回 -32603,异常通知不终止进程。
|
||
|
||
- [ ] **Step 6: 提交异步取消改动**
|
||
|
||
```bash
|
||
git add lsp-server/src/core/server.cppm lsp-server/src/core/dispatcher.cppm lsp-server/test/test_provider/core_server_fixture.cppm lsp-server/test/test_core_server.py
|
||
git diff --cached --check
|
||
git commit -m ":bug: fix(core): dispatch cancellable requests asynchronously"
|
||
```
|
||
|
||
### Task 4: 严格验证 LSP framing
|
||
|
||
**Files:**
|
||
|
||
- Modify: `lsp-server/test/test_core_server.py`
|
||
- Modify: `lsp-server/src/core/server.cppm`
|
||
|
||
- [ ] **Step 1: 写 framing RED 测试**
|
||
|
||
对每个 payload 启动独立 fixture 进程并要求非零退出、stdout 无 LSP 响应:
|
||
|
||
```python
|
||
invalid_payloads = [
|
||
b"Content-Length: 2junk\r\n\r\n{}",
|
||
b"Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}",
|
||
b"Content-Length: 0\r\n\r\n",
|
||
b"Content-Length: 16777217\r\n\r\n",
|
||
b"Content-Length: 10\r\n\r\n{}",
|
||
b"Content-Length: 2\n\n{}",
|
||
b"X-Length: 2\r\n\r\n{}",
|
||
b"Content-Length: 2\r\nContent-Type: application/json\r\n\r\n{}",
|
||
]
|
||
for payload in invalid_payloads:
|
||
result = run_raw(server, payload)
|
||
assert result.returncode == 1
|
||
assert result.stdout == b""
|
||
```
|
||
|
||
另加规范 `Content-Type: application/vscode-jsonrpc; charset=utf-8` 和连续两条
|
||
消息的通过用例,并把 `assert_framing(server)` 加入脚本 `main()`。
|
||
|
||
- [ ] **Step 2: 运行测试并确认 RED**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure
|
||
```
|
||
|
||
Expected: FAIL;部分数字、重复长度或 LF-only header 被旧读取器接受。
|
||
|
||
- [ ] **Step 3: 用明确读取结果和 from_chars 实现严格 framing**
|
||
|
||
```cpp
|
||
enum class ReadStatus
|
||
{
|
||
kMessage,
|
||
kEndOfStream,
|
||
kFramingError,
|
||
};
|
||
|
||
struct ReadResult
|
||
{
|
||
ReadStatus status;
|
||
std::string message;
|
||
};
|
||
|
||
static constexpr std::size_t kMaxMessageSize = 16U * 1024U * 1024U;
|
||
```
|
||
|
||
每个 header 行必须以 `\r` 结束;移除该字符后解析。`Content-Length: ` 后的值
|
||
非空,`from_chars` 必须满足 `error == std::errc{}` 且 pointer 到达字符串末尾。
|
||
只允许一个 Content-Length 和最多一个精确规范 Content-Type。未知 header、空
|
||
header、零/超限长度和短 body 返回 `kFramingError`。
|
||
|
||
主循环处理:
|
||
|
||
```cpp
|
||
const auto read = ReadMessage();
|
||
if (read.status != ReadStatus::kMessage)
|
||
{
|
||
exit_code_ = 1;
|
||
break;
|
||
}
|
||
HandleMessage(read.message);
|
||
```
|
||
|
||
删除 `stoul`、nullopt 重试、5ms sleep 和 framing 错误后的继续读取。
|
||
|
||
- [ ] **Step 4: 运行 GREEN 并提交**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure
|
||
python lsp-server/test/run_lsp_json_tests.py --server lsp-server/build/codex43-clean/Release/src/tsl-server --no-validate
|
||
```
|
||
|
||
Expected: PASS;非法 framing 全部非零退出且无响应,规范连续消息通过。
|
||
|
||
Commit:
|
||
|
||
```bash
|
||
git add lsp-server/src/core/server.cppm lsp-server/test/test_core_server.py
|
||
git diff --cached --check
|
||
git commit -m ":bug: fix(core): reject invalid LSP framing"
|
||
```
|
||
|
||
### Task 5: 将 diagnostics 字节列转换为 UTF-16
|
||
|
||
**Files:**
|
||
|
||
- Modify: `lsp-server/test/test_provider/text_coordinates_test.cppm`
|
||
- Modify: `lsp-server/src/utils/text_coordinates.cppm`
|
||
- Modify: `lsp-server/src/core/server.cppm`
|
||
|
||
- [ ] **Step 1: 写反向坐标 RED 测试**
|
||
|
||
```cpp
|
||
TestResult TextCoordinatesTests::TestBytePointsToUtf16Positions()
|
||
{
|
||
const protocol::string content = "A中😀Z\n😀x";
|
||
|
||
ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 0U}, content),
|
||
0U, 0U);
|
||
ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 4U}, content),
|
||
0U, 2U);
|
||
ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 8U}, content),
|
||
0U, 4U);
|
||
ExpectLspPosition(utils::text_coordinates::ToPosition({1U, 4U}, content),
|
||
1U, 2U);
|
||
ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 6U}, content),
|
||
0U, 2U);
|
||
return {"", true, "ok"};
|
||
}
|
||
```
|
||
|
||
在测试类声明和注册入口分别加入:
|
||
|
||
```cpp
|
||
static TestResult TestBytePointsToUtf16Positions();
|
||
runner.addTest("text coordinates convert byte points to UTF-16 positions",
|
||
TestBytePointsToUtf16Positions);
|
||
```
|
||
|
||
测试 helper 只比较字段:
|
||
|
||
```cpp
|
||
void ExpectLspPosition(const protocol::Position& actual,
|
||
protocol::uinteger line,
|
||
protocol::uinteger character)
|
||
{
|
||
assertEqual(line, actual.line, "LSP line should match");
|
||
assertEqual(character, actual.character, "LSP UTF-16 character should match");
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试并确认 RED**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target test_provider -j1
|
||
```
|
||
|
||
Expected: FAIL,`ToPosition(TSPoint, string)` 尚未定义。
|
||
|
||
- [ ] **Step 3: 实现单一 UTF-8 解码路径的反向转换**
|
||
|
||
导出:
|
||
|
||
```cpp
|
||
protocol::Position ToPosition(TSPoint point,
|
||
const protocol::string& content);
|
||
```
|
||
|
||
实现先扫描到 `point.row` 行首,再逐个调用已有 `DecodeCharacter`。只有完整字符的
|
||
末尾不超过目标 byte column 时才累计 `utf16_units`;目标落在字符内部时停止,
|
||
超出行尾时收敛到行尾。返回实际行号和累计 UTF-16 units,不复制第二套 UTF-8
|
||
判定代码。
|
||
|
||
`PublishDiagnostics` 转换起止点:
|
||
|
||
```cpp
|
||
diagnostic.range.start = utils::text_coordinates::ToPosition(
|
||
TSPoint{error.location.start_line, error.location.start_column}, content);
|
||
diagnostic.range.end = utils::text_coordinates::ToPosition(
|
||
TSPoint{error.location.end_line, error.location.end_column}, content);
|
||
```
|
||
|
||
- [ ] **Step 4: 运行 GREEN 并提交**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure
|
||
```
|
||
|
||
Expected: PASS;中文和 emoji 坐标符合 UTF-16。
|
||
|
||
Commit:
|
||
|
||
```bash
|
||
git add lsp-server/src/utils/text_coordinates.cppm lsp-server/src/core/server.cppm lsp-server/test/test_provider/text_coordinates_test.cppm
|
||
git diff --cached --check
|
||
git commit -m ":bug: fix(core): convert diagnostics to UTF-16 positions"
|
||
```
|
||
|
||
### Task 6: 格式化、全量验证和无效兜底专项复审
|
||
|
||
**Files:**
|
||
|
||
- Modify: `memory-bank/progress.md`
|
||
- Format: 本 Plan 已修改的 C++ 文件
|
||
|
||
- [ ] **Step 1: 格式化本 Plan 修改的 C++ 文件**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
clang-format -i lsp-server/src/core/dispatcher.cppm lsp-server/src/core/server.cppm lsp-server/src/provider/base/interface.cppm lsp-server/src/provider/initialize/initialize.cppm lsp-server/src/provider/manifest.cppm lsp-server/src/utils/text_coordinates.cppm lsp-server/test/test_provider/core_server_fixture.cppm lsp-server/test/test_provider/test_main.cppm lsp-server/test/test_provider/provider_misc_test.cppm lsp-server/test/test_provider/provider_surface_test.cppm lsp-server/test/test_provider/json_provider_coverage_test.cppm lsp-server/test/test_provider/text_coordinates_test.cppm
|
||
```
|
||
|
||
检查格式化没有改动用户的无关 hunk;若有,恢复那些 hunk 到格式化前内容,仅
|
||
保留本 Plan 行。
|
||
|
||
- [ ] **Step 2: 构建所有影响目标**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cmake --build lsp-server/build/codex43-clean/Release --target tsl-server test_scheduler test_provider -j1
|
||
```
|
||
|
||
Expected: exit 0,无编译错误。
|
||
|
||
- [ ] **Step 3: 运行完整相关测试门**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_scheduler|test_provider|test_core_server|test_cli_startup)$' --output-on-failure
|
||
python lsp-server/test/run_lsp_json_tests.py --server lsp-server/build/codex43-clean/Release/src/tsl-server --no-validate
|
||
```
|
||
|
||
Expected: 4/4 CTest PASS,LSP transport smoke exit 0。已知
|
||
`test_ast_script`、`test_symbol_script`、`test_semantic_script` 以及 rename
|
||
结果校验不在本 Plan gate 中。
|
||
|
||
- [ ] **Step 4: 专项搜索无效兜底、兼容层和死代码**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
rg -n 'return "\{\}"|value_or\("\{\}"\)|TO[D]O|RequiresSyncProcessing|CanProcessRequest|SetRequestScheduler|SetManagerHub|ServerLifecycleEvent|LifecycleCallback|provider\.(shutdown|exit|cancel_request)' lsp-server/src/core lsp-server/src/provider lsp-server/test/test_provider
|
||
```
|
||
|
||
Expected: 无命中。再分别检查生产和测试 sleep:
|
||
|
||
```bash
|
||
rg -n 'sleep_for' lsp-server/src/core
|
||
rg -n 'sleep_for' lsp-server/test/test_provider/core_server_fixture.cppm
|
||
```
|
||
|
||
Expected: core 无命中;fixture 恰好命中确定性阻塞 Provider。再运行:
|
||
|
||
```bash
|
||
rg -n 'catch\s*\(\.\.\.\)|catch\s*\(const std::exception' lsp-server/src/core
|
||
```
|
||
|
||
Expected: 每个 catch 都能对应“请求转 InternalError”“通知只记录”或“主循环致命
|
||
错误退出”,不存在捕获后 sleep/继续的分支。
|
||
|
||
- [ ] **Step 5: 检查最终 diff 和提交遗漏**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
git diff --check
|
||
git status --short
|
||
git log -6 --oneline
|
||
```
|
||
|
||
Expected: 本 Plan 产品/测试改动已提交;用户原有未提交文件仍存在且未混入提交;
|
||
新增提交均带匹配 type 的 emoji。
|
||
|
||
若格式化或复审产生必要修正,先重新执行 Step 2/3,再提交:
|
||
|
||
```bash
|
||
git commit -m ":art: style(core): normalize lifecycle implementation"
|
||
```
|
||
|
||
- [ ] **Step 6: 写回主循环状态**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
python docs/standards/playbook/scripts/main_loop.py finish -plan docs/superpowers/plans/2026-07-13-lsp-core-lifecycle.md -status done -progress memory-bank/progress.md
|
||
```
|
||
|
||
更新 `memory-bank/progress.md` 上半部分摘要,记录生命周期状态机、请求取消、
|
||
framing、UTF-16 diagnostics 和验证结果;只暂存该文件并提交:
|
||
|
||
```bash
|
||
git add memory-bank/progress.md
|
||
git diff --cached --check
|
||
git commit -m ":memo: docs(progress): finish LSP core lifecycle plan"
|
||
```
|