diff --git a/docs/superpowers/plans/2026-05-24-conan-dependency-upgrade.md b/docs/superpowers/plans/2026-05-24-conan-dependency-upgrade.md new file mode 100644 index 0000000..08f56ef --- /dev/null +++ b/docs/superpowers/plans/2026-05-24-conan-dependency-upgrade.md @@ -0,0 +1,114 @@ +# Conan Dependency Upgrade 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:** Upgrade `lsp-server` Conan dependencies to the latest versions available on ConanCenter and adapt the project until it builds and passes targeted verification. + +**Architecture:** Treat ConanCenter as the version authority for this task. Update only the versions that have newer published recipes, regenerate the build with the existing profiles, and make the smallest necessary source/build changes to restore compatibility. Keep unchanged packages pinned when ConanCenter has no newer recipe. + +**Tech Stack:** Conan 2, CMake 4.2, Ninja, Clang toolchain, C++23 Modules + +--- + +## Plan Meta + +- **Plan Group:** deps-upgrade +- **Parent Plan:** none +- **Verification Scope:** local +- **Verification Gate:** must-pass + +### Task 1: Update Conan dependency declarations + +**Files:** + +- Modify: `lsp-server/conanfile.txt` + +- [x] Confirm the latest ConanCenter recipes: + `glaze/7.4.0`, `spdlog/1.17.0`, `fmt/12.1.0`, + `taskflow/4.0.0`, `tree-sitter/0.25.9` +- [x] Update `lsp-server/conanfile.txt`: + `glaze/7.0.2 -> glaze/7.4.0` + `taskflow/3.10.0 -> taskflow/4.0.0` +- [x] Leave `spdlog/1.17.0`, `fmt/12.1.0`, and `tree-sitter/0.25.9` + unchanged because ConanCenter does not expose newer recipes + +### Task 2: Regenerate dependencies and capture compatibility failures + +**Files:** + +- Modify: `lsp-server/build/clang-linux/Release/**` (generated) +- Inspect: `lsp-server/src/**` +- Inspect: `lsp-server/test/**` + +- [x] Run Conan install with the existing Linux Clang profile: + +```bash +CONAN_HOME=/tmp/conan-home conan install lsp-server \ + -pr:h=lsp-server/conan/profiles/linux-x86_64-clang \ + -pr:b=lsp-server/conan/profiles/linux-x86_64-clang \ + -of lsp-server/build/clang-linux/Release \ + --build=missing +``` + +- [x] Reconfigure the existing build directory: + +```bash +cmake -S lsp-server -B lsp-server/build/clang-linux/Release -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=$PWD/lsp-server/build/clang-linux/Release/generators/conan_toolchain.cmake \ + -DBUILD_TESTS=ON +``` + +- [x] Build to identify required source-level adaptations: + +```bash +cmake --build lsp-server/build/clang-linux/Release +``` + +### Task 3: Adapt code only where the upgraded APIs require it + +**Files:** + +- Modify: exact files reported by the build, likely under + `lsp-server/src/bridge/`, `lsp-server/src/codec/`, + `lsp-server/src/core/`, or related test targets + +- [x] For `glaze` breakages, update serialization/meta/JSON-RPC call sites + to the current `glaze/7.4.0` API with the smallest possible diff +- [x] For `taskflow` breakages, update executor/task API usage to the + `taskflow/4.0.0` API with the smallest possible diff +- [x] Rebuild after each focused fix until the full build succeeds: + +```bash +cmake --build lsp-server/build/clang-linux/Release +``` + +### Task 4: Run targeted verification and summarize the upgrade result + +**Files:** + +- Verify: `lsp-server/conanfile.txt` +- Verify: any source files touched in Task 3 + +- [x] Run dependency-focused tests: + +```bash +ctest --test-dir lsp-server/build/clang-linux/Release \ + -R 'test_ast|test_provider|test_semantic|test_symbol|test_scheduler' \ + --output-on-failure +``` + +- [x] Run whitespace / patch hygiene check: + +```bash +git -c core.trustctime=false -c core.checkStat=minimal diff --check +``` + +- [x] Report three facts in the final handoff: + upgraded versions, any compatibility edits made, and the exact + verification commands that passed + +Known residual verification: + +- `test_ast_script`, `test_symbol_script`, and `test_semantic_script` still fail on existing + TSF fixture parse/load errors under `lsp-server/test/test_tree_sitter/test`. + Dependency-focused targets `test_lsp_any`, `test_provider`, and `test_scheduler` pass. diff --git a/lsp-server/CMakeLists.txt b/lsp-server/CMakeLists.txt index 46c9566..761e73a 100644 --- a/lsp-server/CMakeLists.txt +++ b/lsp-server/CMakeLists.txt @@ -1,6 +1,11 @@ cmake_minimum_required(VERSION 4.2) -set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") +# CMake 4.3 rotated the experimental gate UUID for `import std`. +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.3") + set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "451f2fe2-a8a2-47c3-bc32-94786d8fc91b") +else() + set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") +endif() project(tsl-server LANGUAGES C CXX) diff --git a/lsp-server/conanfile.txt b/lsp-server/conanfile.txt index bd92893..2fbae7a 100644 --- a/lsp-server/conanfile.txt +++ b/lsp-server/conanfile.txt @@ -1,8 +1,8 @@ [requires] -glaze/7.0.2 +glaze/7.4.0 spdlog/1.17.0 fmt/12.1.0 -taskflow/3.10.0 +taskflow/4.0.0 tree-sitter/0.25.9 [generators] diff --git a/lsp-server/src/bridge/taskflow.cppm b/lsp-server/src/bridge/taskflow.cppm index d21b33f..ee3784e 100644 --- a/lsp-server/src/bridge/taskflow.cppm +++ b/lsp-server/src/bridge/taskflow.cppm @@ -1,6 +1,7 @@ module; // Global module fragment: pull in third-party headers +#include #include export module taskflow; diff --git a/lsp-server/src/codec/transformer.cppm b/lsp-server/src/codec/transformer.cppm index cea3e24..6659241 100644 --- a/lsp-server/src/codec/transformer.cppm +++ b/lsp-server/src/codec/transformer.cppm @@ -29,6 +29,9 @@ export namespace lsp::codec template static protocol::LSPAny SerializeViaJson(const T& obj); + + template + static T ConvertEnum(const protocol::LSPAny& any); }; } @@ -165,6 +168,14 @@ namespace lsp::codec return std::nullopt; return FromLSPAny(any); } + else if constexpr (std::is_enum_v) + { + return ConvertEnum(any); + } + else if constexpr (requires { from_lsp_any_custom(std::type_identity{}, any); }) + { + return from_lsp_any_custom(std::type_identity{}, any); + } else if constexpr (is_user_struct_v) { return ConvertViaJson(any); @@ -198,6 +209,13 @@ namespace lsp::codec throw ConversionError("LSPAny does not contain a compatible numeric type"); } + template + T LSPAnyConverter::ConvertEnum(const protocol::LSPAny& any) + { + using Underlying = std::underlying_type_t; + return static_cast(ExtractNumber(any)); + } + template T LSPAnyConverter::ConvertViaJson(const protocol::LSPAny& any) { diff --git a/lsp-server/src/protocol/text_document/completion.cppm b/lsp-server/src/protocol/text_document/completion.cppm index 945ab4b..3e39d1e 100644 --- a/lsp-server/src/protocol/text_document/completion.cppm +++ b/lsp-server/src/protocol/text_document/completion.cppm @@ -241,3 +241,167 @@ export namespace lsp::protocol std::vector items; }; } + +export namespace lsp::protocol +{ + namespace completion_conversion_detail + { + inline const LSPObject& RequireObject(const LSPAny& any, std::string_view context) + { + if (!any.Is()) + throw std::runtime_error(std::string(context) + " must be an object"); + return any.Get(); + } + + inline const LSPArray& RequireArray(const LSPAny& any, std::string_view context) + { + if (!any.Is()) + throw std::runtime_error(std::string(context) + " must be an array"); + return any.Get(); + } + + inline const LSPAny* FindField(const LSPObject& obj, std::string_view key) + { + auto it = obj.find(string(key)); + if (it == obj.end() || it->second.Is()) + return nullptr; + return &it->second; + } + + inline string ReadString(const LSPAny& any, std::string_view context) + { + if (!any.Is()) + throw std::runtime_error(std::string(context) + " must be a string"); + return any.Get(); + } + + inline boolean ReadBoolean(const LSPAny& any, std::string_view context) + { + if (!any.Is()) + throw std::runtime_error(std::string(context) + " must be a boolean"); + return any.Get(); + } + + inline std::int64_t ReadInteger(const LSPAny& any, std::string_view context) + { + if (any.Is()) + return any.Get(); + if (any.Is()) + return any.Get(); + throw std::runtime_error(std::string(context) + " must be an integer"); + } + + template + std::optional OptionalString(const LSPObject& obj, T key) + { + if (const auto* field = FindField(obj, key)) + return ReadString(*field, key); + return std::nullopt; + } + + template + std::optional OptionalBoolean(const LSPObject& obj, T key) + { + if (const auto* field = FindField(obj, key)) + return ReadBoolean(*field, key); + return std::nullopt; + } + + template + std::optional OptionalEnum(const LSPObject& obj, T key) + { + if (const auto* field = FindField(obj, key)) + return static_cast(ReadInteger(*field, key)); + return std::nullopt; + } + + template + std::optional> OptionalStringVector(const LSPObject& obj, T key) + { + const auto* field = FindField(obj, key); + if (field == nullptr) + return std::nullopt; + + const auto& arr = RequireArray(*field, key); + std::vector values; + values.reserve(arr.size()); + for (const auto& item : arr) + values.push_back(ReadString(item, key)); + return values; + } + } + + inline CompletionItemLabelDetails from_lsp_any_custom(std::type_identity, + const LSPAny& any) + { + const auto& obj = completion_conversion_detail::RequireObject(any, "CompletionItemLabelDetails"); + + CompletionItemLabelDetails details; + details.detail = completion_conversion_detail::OptionalString(obj, "detail"); + details.description = completion_conversion_detail::OptionalString(obj, "description"); + return details; + } + + inline CompletionItem from_lsp_any_custom(std::type_identity, const LSPAny& any) + { + const auto& obj = completion_conversion_detail::RequireObject(any, "CompletionItem"); + + CompletionItem item; + const auto* label = completion_conversion_detail::FindField(obj, "label"); + if (label == nullptr) + throw std::runtime_error("Missing required field: label"); + + item.label = completion_conversion_detail::ReadString(*label, "label"); + if (const auto* label_details = completion_conversion_detail::FindField(obj, "labelDetails")) + item.labelDetails = from_lsp_any_custom(std::type_identity{}, *label_details); + item.kind = completion_conversion_detail::OptionalEnum(obj, "kind"); + item.detail = completion_conversion_detail::OptionalString(obj, "detail"); + item.preselect = completion_conversion_detail::OptionalBoolean(obj, "preselect"); + item.sortText = completion_conversion_detail::OptionalString(obj, "sortText"); + item.filterText = completion_conversion_detail::OptionalString(obj, "filterText"); + item.insertText = completion_conversion_detail::OptionalString(obj, "insertText"); + item.insertTextFormat = completion_conversion_detail::OptionalEnum(obj, "insertTextFormat"); + item.insertTextMode = completion_conversion_detail::OptionalEnum(obj, "insertTextMode"); + item.textEditText = completion_conversion_detail::OptionalString(obj, "textEditText"); + item.commitCharacters = completion_conversion_detail::OptionalStringVector(obj, "commitCharacters"); + + if (const auto* data = completion_conversion_detail::FindField(obj, "data")) + item.data = *data; + + return item; + } + + inline CompletionList from_lsp_any_custom(std::type_identity, const LSPAny& any) + { + const auto& obj = completion_conversion_detail::RequireObject(any, "CompletionList"); + + CompletionList list; + const auto* is_incomplete = completion_conversion_detail::FindField(obj, "isIncomplete"); + if (is_incomplete == nullptr) + throw std::runtime_error("Missing required field: isIncomplete"); + list.isIncomplete = completion_conversion_detail::ReadBoolean(*is_incomplete, "isIncomplete"); + + if (const auto* items = completion_conversion_detail::FindField(obj, "items")) + { + const auto& arr = completion_conversion_detail::RequireArray(*items, "items"); + list.items.reserve(arr.size()); + for (const auto& item : arr) + list.items.push_back(from_lsp_any_custom(std::type_identity{}, item)); + } + + if (const auto* item_defaults = completion_conversion_detail::FindField(obj, "itemDefaults")) + { + const auto& defaults_obj = completion_conversion_detail::RequireObject(*item_defaults, "itemDefaults"); + list.itemDefaults.commitCharacters = + completion_conversion_detail::OptionalStringVector(defaults_obj, "commitCharacters"); + list.itemDefaults.insertTextFormat = + completion_conversion_detail::OptionalEnum(defaults_obj, "insertTextFormat"); + list.itemDefaults.insertTextMode = + completion_conversion_detail::OptionalEnum(defaults_obj, "insertTextMode"); + if (const auto* data = completion_conversion_detail::FindField(defaults_obj, "data")) + list.itemDefaults.data = *data; + } + + return list; + } +} diff --git a/lsp-server/src/scheduler/async_executor.cppm b/lsp-server/src/scheduler/async_executor.cppm index a8314b4..8a2ba14 100644 --- a/lsp-server/src/scheduler/async_executor.cppm +++ b/lsp-server/src/scheduler/async_executor.cppm @@ -16,6 +16,7 @@ export namespace lsp::scheduler std::mutex mutex; std::condition_variable cv; bool completed = false; + bool callback_completed = false; std::optional result; std::exception_ptr error; std::chrono::steady_clock::time_point start_time{}; @@ -141,7 +142,7 @@ namespace lsp::scheduler if (!state) return false; std::unique_lock lk(state->mutex); - state->cv.wait(lk, [state]() { return state->completed; }); + state->cv.wait(lk, [state]() { return state->completed && state->callback_completed; }); return true; } @@ -151,7 +152,7 @@ namespace lsp::scheduler if (!state) return std::nullopt; std::unique_lock lk(state->mutex); - if (!state->completed || state->error) + if (!state->completed || !state->callback_completed || state->error) return std::nullopt; return state->result; } @@ -213,7 +214,7 @@ namespace lsp::scheduler } std::unique_lock lk(state->mutex); - state->cv.wait(lk, [state]() { return state->completed; }); + state->cv.wait(lk, [state]() { return state->completed && state->callback_completed; }); return true; } @@ -231,7 +232,7 @@ namespace lsp::scheduler for (const auto& state : tasks) { std::unique_lock lk(state->mutex); - state->cv.wait(lk, [state]() { return state->completed; }); + state->cv.wait(lk, [state]() { return state->completed && state->callback_completed; }); } // Ensure the underlying executor finishes any tasks that may have been @@ -334,22 +335,42 @@ namespace lsp::scheduler state->error = std::make_exception_ptr(std::runtime_error("Task failed")); } - state->cv.notify_all(); - UnregisterTask(task_id, state); auto elapsed = GetElapsedTime(start_time); - if (failed) + bool callback_failed = false; + if (callback) + { + try + { + callback(result, is_cancelled); + } + catch (...) + { + callback_failed = true; + std::unique_lock lk(state->mutex); + state->error = std::current_exception(); + } + } + + const bool has_failed = failed || callback_failed; + if (has_failed) ++failed_; else if (is_cancelled) ++cancelled_; else ++completed_; - if (callback) - callback(result, is_cancelled); + { + std::unique_lock lk(state->mutex); + state->callback_completed = true; + } + state->cv.notify_all(); - spdlog::info("[{}] Task completed. cancelled={}, failed={}, elapsed={}", task_id, is_cancelled, failed, FormatDuration(elapsed)); + if (callback_failed) + spdlog::error("[{}] Task callback threw exception", task_id); + + spdlog::info("[{}] Task completed. cancelled={}, failed={}, elapsed={}", task_id, is_cancelled, has_failed, FormatDuration(elapsed)); } bool AsyncExecutor::RegisterTask(const std::string& task_id, const detail::ActiveEntry& ctx) diff --git a/lsp-server/test/test_lsp_any/transformer_test.cppm b/lsp-server/test/test_lsp_any/transformer_test.cppm index 95c2be8..cb5a057 100644 --- a/lsp-server/test/test_lsp_any/transformer_test.cppm +++ b/lsp-server/test/test_lsp_any/transformer_test.cppm @@ -6,7 +6,10 @@ export module lsp.test.lsp_any.transformer; import lsp.test.framework; import lsp.protocol.common.basic_types; +import lsp.protocol.common.message; +import lsp.protocol.text_document.completion; import lsp.codec.common; +import lsp.codec.facade; import lsp.codec.transformer; export namespace lsp::test @@ -69,6 +72,7 @@ export namespace lsp::test static TestResult testNestedVector(); static TestResult testNestedLSPObject(); static TestResult testMixedTypeNesting(); + static TestResult testCompletionListWithDataObject(); }; } @@ -124,6 +128,7 @@ namespace lsp::test runner.addTest("Transformer - 嵌套Vector", testNestedVector); runner.addTest("Transformer - 嵌套LSPObject", testNestedLSPObject); runner.addTest("Transformer - 混合类型嵌套", testMixedTypeNesting); + runner.addTest("Transformer - CompletionList data对象", testCompletionListWithDataObject); } // ==================== ToLSPAny 基本类型测试 ==================== @@ -717,4 +722,32 @@ namespace lsp::test return result; } + TestResult TransformerTests::testCompletionListWithDataObject() + { + TestResult result; + result.passed = true; + + const auto response_json = std::string{ + R"json({"jsonrpc":"2.0","id":"c1","result":{"isIncomplete":false,"itemDefaults":{},"items":[{"data":{"class":"Widget","ctx":"call","is_static":true,"kind":"method","name":"StaticFoo","unit":"MainUnit","uri":"file:///home/csh/windows_share/tinysoft/tsl-devkit/lsp-server/test/test_provider/fixtures/main_unit.tsf"},"kind":2,"label":"StaticFoo","labelDetails":{"description":"[E]","detail":"(y: integer)"}}]}})json" + }; + auto response = transform::Deserialize(response_json); + + assertTrue(response.has_value(), "应该能反序列化 ResponseMessage"); + assertTrue(response->result.has_value(), "ResponseMessage.result 应该有值"); + + auto completion_list = transform::FromLSPAny.template operator()(response->result.value()); + + assertEqual(size_t(1), completion_list.items.size(), "应该保留一个补全项"); + assertTrue(completion_list.items[0].data.has_value(), "CompletionItem.data 应该有值"); + assertTrue(completion_list.items[0].data->Is(), "CompletionItem.data 应该是对象"); + + const auto& result_data = completion_list.items[0].data->Get(); + assertEqual(std::string("Widget"), result_data.at("class").Get(), "data.class 应该保留"); + assertEqual(std::string("call"), result_data.at("ctx").Get(), "data.ctx 应该保留"); + assertEqual(true, result_data.at("is_static").Get(), "data.is_static 应该保留"); + + result.message = "成功"; + return result; + } + } // namespace lsp::test diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 1017790..8e12e2d 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -2,8 +2,8 @@ ## Current Focus -- 已完成 playbook 强对齐;后续开发统一使用新 `memory-bank/` 结构、 - `docs/superpowers/plans/` 和 `main_loop.py` 状态流转 +- 已完成 Conan 依赖升级;`lsp-server` 当前使用 ConanCenter 可见的最新 + `glaze`、`taskflow`、`spdlog`、`fmt` 与 `tree-sitter` recipe ## Recent Changes @@ -14,23 +14,30 @@ `docs/prompts/meta/prompt-generator.md`、 `memory-bank/architecture.md`、 `memory-bank/tech-stack.md` 已移除 +- `glaze` 已升级到 `7.4.0`,`taskflow` 已升级到 `4.0.0` +- 已适配 CMake 4.3 `import std` gate、Taskflow 4.0 头文件依赖、 + completion `LSPAny data` 反序列化和 scheduler completion callback 顺序 ## Next Steps 1. 新任务先写到 `docs/superpowers/plans/` 再执行 2. 进入执行阶段前用 `main_loop.py claim` 领取 Plan 3. Playbook 升级保持 subtree + sync 的固定顺序 +4. 另行处理既有脚本类测试失败: + `test_ast_script`、`test_symbol_script`、`test_semantic_script` ## Open Risks - CIFS 共享目录上的 git 索引状态可能误报脏工作区, 影响 subtree、status 和 stash 类命令 +- `lsp-server/test/test_tree_sitter/test` 下部分 TSF fixture 仍存在解析失败, + 会导致脚本类 AST / symbol / semantic CTest 失败 ## Workflow State phase: done -plan: docs/superpowers/plans/2026-05-24-playbook-strong-alignment.md +plan: docs/superpowers/plans/2026-05-24-conan-dependency-upgrade.md executor: executing-plans constraints: karpathy-guidelines,.agents,AGENT_RULES @@ -39,4 +46,5 @@ constraints: karpathy-guidelines,.agents,AGENT_RULES - [x] `2026-05-24-playbook-strong-alignment.md` done +- [x] `2026-05-24-conan-dependency-upgrade.md` done: known-failures:test_ast_script,test_symbol_script,test_semantic_script