Step 96: LSP server configuration
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
158
editor/src/SettingsManager.h
Normal file
158
editor/src/SettingsManager.h
Normal file
@@ -0,0 +1,158 @@
|
||||
#pragma once
|
||||
// Step 96: LSP server configuration settings
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <cstdlib>
|
||||
#include "ast/Schema.h"
|
||||
#include "ast/ASTNode.h"
|
||||
|
||||
struct LSPServerConfig {
|
||||
std::string language;
|
||||
std::string server;
|
||||
std::string path;
|
||||
std::vector<std::string> args;
|
||||
std::string argsLine;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
class SettingsManager {
|
||||
public:
|
||||
SettingsManager() { loadDefaults(); }
|
||||
|
||||
const std::vector<LSPServerConfig>& getLSPServers() const { return lspServers_; }
|
||||
std::vector<LSPServerConfig>& 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<std::string>& 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<LSPServerConfig> 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<std::string>& 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<std::string>& 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<std::string> splitArgs(const std::string& line) {
|
||||
std::vector<std::string> 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<std::string>& 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "LSPClient.h"
|
||||
#include "Pipeline.h"
|
||||
#include "Diagnostics.h"
|
||||
#include "SettingsManager.h"
|
||||
#include "ast/Generator.h"
|
||||
|
||||
#include <cstdio>
|
||||
@@ -100,6 +101,8 @@ struct EditorState {
|
||||
bool analysisPending = false;
|
||||
double analysisLastChange = 0.0;
|
||||
std::vector<EditorDiagnostic> 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
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
39
editor/tests/step96_test.cpp
Normal file
39
editor/tests/step96_test.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <iostream>
|
||||
#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<std::string> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user