diff --git a/PROGRESS.md b/PROGRESS.md index 6af517f..ddccc8d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -195,6 +195,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 93: **IMPLEMENTED** — LSP hover and signature help requests, response parsing, and inline tooltip/popup (2/2 tests pass) - [x] Step 94: **IMPLEMENTED** — Whetstone diagnostics aggregation via Pipeline, merged Problems panel, and gutter markers (1/1 tests pass) - [x] Step 95: **IMPLEMENTED** — Diagnostic gutter markers with tooltips and inline squiggles (1/1 tests pass) +- [x] Step 96: **IMPLEMENTED** — LSP server settings UI with defaults, auto-detect, and schema validation hook (2/2 tests pass) --- @@ -269,6 +270,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 93:** Compile and pass (2/2) **Step 94:** Compile and pass (1/1) **Step 95:** Compile and pass (1/1) +**Step 96:** Compile and pass (2/2) --- @@ -372,3 +374,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Codex | Step 93: LSP hover and signature help requests/response parsing; tooltip and signature popup. 2/2 tests pass. | | 2026-02-09 | Codex | Step 94: Whetstone diagnostics aggregation via Pipeline; merged Problems panel and gutter markers. 1/1 tests pass. | | 2026-02-09 | Codex | Step 95: Diagnostic gutter markers with tooltips and inline squiggles. 1/1 tests pass. | +| 2026-02-09 | Codex | Step 96: LSP server settings UI with defaults, auto-detect, and schema validation hook. 2/2 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 104fef4..7d8b69e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -526,6 +526,10 @@ add_executable(step95_test tests/step95_test.cpp) target_include_directories(step95_test PRIVATE src) target_link_libraries(step95_test PRIVATE imgui::imgui unofficial::tree-sitter::tree-sitter tree_sitter_python tree_sitter_cpp tree_sitter_elisp) +add_executable(step96_test tests/step96_test.cpp) +target_include_directories(step96_test PRIVATE src) +target_link_libraries(step96_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/SettingsManager.h b/editor/src/SettingsManager.h new file mode 100644 index 0000000..9778054 --- /dev/null +++ b/editor/src/SettingsManager.h @@ -0,0 +1,158 @@ +#pragma once +// Step 96: LSP server configuration settings + +#include +#include +#include +#include +#include "ast/Schema.h" +#include "ast/ASTNode.h" + +struct LSPServerConfig { + std::string language; + std::string server; + std::string path; + std::vector args; + std::string argsLine; + bool enabled = true; +}; + +class SettingsManager { +public: + SettingsManager() { loadDefaults(); } + + const std::vector& getLSPServers() const { return lspServers_; } + std::vector& getLSPServersMutable() { return lspServers_; } + + LSPServerConfig* getServer(const std::string& language) { + for (auto& s : lspServers_) { + if (s.language == language) return &s; + } + return nullptr; + } + + void setServerPath(const std::string& language, const std::string& path) { + auto* cfg = getServer(language); + if (cfg) cfg->path = path; + } + + void syncArgs(LSPServerConfig& cfg) { + cfg.args = splitArgs(cfg.argsLine); + } + + void autoDetect() { + for (auto& s : lspServers_) { + if (!s.path.empty()) continue; + std::string found; + if (findOnPath(s.server, found)) { + s.path = found; + } + } + } + + bool validateSchema(const ASTNode* root, std::vector& errors) const { + errors.clear(); + if (!root) { + errors.push_back("Null AST root"); + return false; + } + ASTSchema schema; + validateNode(root, schema, errors); + return errors.empty(); + } + +private: + std::vector lspServers_; + + void loadDefaults() { + lspServers_.push_back(makeConfig("python", "pylsp", {})); + lspServers_.push_back(makeConfig("cpp", "clangd", {})); + lspServers_.push_back(makeConfig("rust", "rust-analyzer", {})); + lspServers_.push_back(makeConfig("go", "gopls", {})); + lspServers_.push_back(makeConfig("javascript", "typescript-language-server", {"--stdio"})); + lspServers_.push_back(makeConfig("typescript", "typescript-language-server", {"--stdio"})); + lspServers_.push_back(makeConfig("java", "jdtls", {})); + } + + static LSPServerConfig makeConfig(const std::string& lang, + const std::string& server, + const std::vector& args) { + LSPServerConfig cfg; + cfg.language = lang; + cfg.server = server; + cfg.args = args; + cfg.argsLine = joinArgs(args); + cfg.enabled = true; + return cfg; + } + + static std::string joinArgs(const std::vector& args) { + std::string out; + for (size_t i = 0; i < args.size(); ++i) { + if (i > 0) out += " "; + out += args[i]; + } + return out; + } + + static std::vector splitArgs(const std::string& line) { + std::vector out; + std::string current; + for (char c : line) { + if (c == ' ') { + if (!current.empty()) { + out.push_back(current); + current.clear(); + } + } else { + current.push_back(c); + } + } + if (!current.empty()) out.push_back(current); + return out; + } + + static bool findOnPath(const std::string& exe, std::string& outPath) { + const char* pathEnv = std::getenv("PATH"); + if (!pathEnv) return false; + std::string pathVar(pathEnv); + size_t start = 0; + while (start <= pathVar.size()) { + size_t end = pathVar.find(';', start); + if (end == std::string::npos) end = pathVar.size(); + std::string dir = pathVar.substr(start, end - start); + if (!dir.empty()) { + std::filesystem::path p = std::filesystem::path(dir) / exe; + if (std::filesystem::exists(p)) { + outPath = p.string(); + return true; + } + std::filesystem::path pExe = p; + pExe += ".exe"; + if (std::filesystem::exists(pExe)) { + outPath = pExe.string(); + return true; + } + } + if (end == pathVar.size()) break; + start = end + 1; + } + return false; + } + + static void validateNode(const ASTNode* node, const ASTSchema& schema, std::vector& errors) { + if (!node) return; + for (const auto& role : node->childRoles()) { + const auto& kids = node->getChildren(role); + if (schema.isSingleValued(node->conceptType, role) && kids.size() > 1) { + errors.push_back("Role '" + role + "' on " + node->conceptType + " is single-valued"); + } + for (const auto* child : kids) { + if (!schema.isLegalChild(node->conceptType, role, child->conceptType)) { + errors.push_back("Illegal child " + child->conceptType + " under " + node->conceptType + " role '" + role + "'"); + } + validateNode(child, schema, errors); + } + } + } +}; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 40ee030..290ea2d 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -27,6 +27,7 @@ #include "LSPClient.h" #include "Pipeline.h" #include "Diagnostics.h" +#include "SettingsManager.h" #include "ast/Generator.h" #include @@ -100,6 +101,8 @@ struct EditorState { bool analysisPending = false; double analysisLastChange = 0.0; std::vector whetstoneDiagnostics; + SettingsManager settings; + bool showLspSettings = false; BufferState* active() { return activeBuffer; } @@ -471,6 +474,15 @@ static bool InputTextMultilineStr(const char* label, std::string* str, size, flags, InputTextCallback, &cbData); } +static bool InputTextStr(const char* label, std::string* str, ImGuiInputTextFlags flags = 0) { + flags |= ImGuiInputTextFlags_CallbackResize; + InputTextCallbackData cbData{str}; + if (str->capacity() < str->size() + 64) + str->reserve(str->size() + 256); + return ImGui::InputText(label, str->data(), str->capacity() + 1, + flags, InputTextCallback, &cbData); +} + // --------------------------------------------------------------------------- // Theme setup — VSCode Dark-inspired // --------------------------------------------------------------------------- @@ -894,6 +906,7 @@ int main(int, char**) { if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Show Whitespace", nullptr, &state.showWhitespace); ImGui::MenuItem("Show Minimap", nullptr, &state.showMinimap); + ImGui::MenuItem("LSP Servers...", nullptr, &state.showLspSettings); ImGui::EndMenu(); } if (ImGui::BeginMenu("Language")) { @@ -979,6 +992,34 @@ int main(int, char**) { ImGui::End(); } + // --------------------------------------------------------------- + // LSP Server Settings + // --------------------------------------------------------------- + if (state.showLspSettings) { + ImGui::Begin("LSP Servers", &state.showLspSettings); + ImGui::PushFont(uiFont); + if (ImGui::Button("Auto-Detect")) { + state.settings.autoDetect(); + } + ImGui::Separator(); + for (auto& cfg : state.settings.getLSPServersMutable()) { + ImGui::PushID(cfg.language.c_str()); + ImGui::Checkbox("Enabled", &cfg.enabled); + ImGui::SameLine(); + ImGui::Text("%s", cfg.language.c_str()); + ImGui::SetNextItemWidth(320); + InputTextStr("Path", &cfg.path); + ImGui::SetNextItemWidth(320); + if (InputTextStr("Args", &cfg.argsLine)) { + state.settings.syncArgs(cfg); + } + ImGui::Separator(); + ImGui::PopID(); + } + ImGui::PopFont(); + ImGui::End(); + } + // --------------------------------------------------------------- // Editor (center) — editable text area // --------------------------------------------------------------- diff --git a/editor/tests/step96_test.cpp b/editor/tests/step96_test.cpp new file mode 100644 index 0000000..9714adc --- /dev/null +++ b/editor/tests/step96_test.cpp @@ -0,0 +1,39 @@ +// Step 96 TDD Test: LSP server configuration settings +// +// Tests: +// 1. Default LSP configs are present +// 2. Schema validation flags illegal child + +#include +#include +#include "SettingsManager.h" +#include "ast/Module.h" +#include "ast/Function.h" + +int main() { + int passed = 0; + int failed = 0; + + SettingsManager settings; + auto* py = settings.getServer("python"); + auto* cpp = settings.getServer("cpp"); + assert(py && cpp); + assert(py->server == "pylsp"); + assert(cpp->server == "clangd"); + std::cout << "Test 1 PASS: default LSP configs" << std::endl; + ++passed; + + Module* mod = new Module("m1", "mod", "python"); + Function* fn = new Function("f1", "foo"); + mod->addChild("variables", fn); // illegal: variables should be Variable + + std::vector errors; + bool ok = settings.validateSchema(mod, errors); + assert(!ok); + assert(!errors.empty()); + std::cout << "Test 2 PASS: schema validation catches illegal child" << std::endl; + ++passed; + + std::cout << "\n=== Step 96 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}