Step 91: LSP diagnostics integration

This commit is contained in:
Bill
2026-02-09 09:55:48 -07:00
parent 9083e05471
commit e3439b2a3d
5 changed files with 244 additions and 0 deletions

View File

@@ -6,6 +6,7 @@
#include <string>
#include <memory>
#include <vector>
#include <unordered_map>
#include <nlohmann/json.hpp>
class LSPTransport {
@@ -22,6 +23,24 @@ public:
explicit LSPClient(std::shared_ptr<LSPTransport> transport)
: transport_(std::move(transport)) {}
struct Position {
int line = 0;
int character = 0;
};
struct Range {
Position start;
Position end;
};
struct Diagnostic {
std::string uri;
Range range;
int severity = 0;
std::string message;
std::string source;
};
void initialize(const std::string& rootUri,
const std::string& clientName,
int processId) {
@@ -40,6 +59,93 @@ public:
if (transport_) transport_->close();
}
void didOpen(const std::string& uri,
const std::string& languageId,
const std::string& text,
int version) {
nlohmann::json params;
params["textDocument"] = {
{"uri", uri},
{"languageId", languageId},
{"version", version},
{"text", text}
};
sendNotification("textDocument/didOpen", params);
}
void didChange(const std::string& uri,
const std::string& text,
int version) {
nlohmann::json params;
params["textDocument"] = {
{"uri", uri},
{"version", version}
};
params["contentChanges"] = nlohmann::json::array({{
{"text", text}
}});
sendNotification("textDocument/didChange", params);
}
void didSave(const std::string& uri, const std::string& text = std::string()) {
nlohmann::json params;
params["textDocument"] = {{"uri", uri}};
if (!text.empty()) params["text"] = text;
sendNotification("textDocument/didSave", params);
}
void handleMessage(const std::string& msg) {
nlohmann::json j;
try {
j = nlohmann::json::parse(msg);
} catch (...) {
return;
}
if (!j.contains("method")) return;
if (j["method"].get<std::string>() != "textDocument/publishDiagnostics") return;
if (!j.contains("params")) return;
const auto& params = j["params"];
if (!params.contains("uri") || !params.contains("diagnostics")) return;
std::string uri = params["uri"].get<std::string>();
std::vector<Diagnostic> parsed;
for (const auto& diag : params["diagnostics"]) {
Diagnostic d;
d.uri = uri;
if (diag.contains("range")) {
const auto& range = diag["range"];
if (range.contains("start")) {
d.range.start.line = range["start"].value("line", 0);
d.range.start.character = range["start"].value("character", 0);
}
if (range.contains("end")) {
d.range.end.line = range["end"].value("line", 0);
d.range.end.character = range["end"].value("character", 0);
}
}
d.severity = diag.value("severity", 0);
d.message = diag.value("message", "");
d.source = diag.value("source", "");
parsed.push_back(std::move(d));
}
diagnosticsByUri_[uri] = std::move(parsed);
}
std::vector<Diagnostic> getDiagnostics() const {
std::vector<Diagnostic> out;
for (const auto& kv : diagnosticsByUri_) {
out.insert(out.end(), kv.second.begin(), kv.second.end());
}
return out;
}
std::vector<Diagnostic> getDiagnosticsForUri(const std::string& uri) const {
auto it = diagnosticsByUri_.find(uri);
if (it == diagnosticsByUri_.end()) return {};
return it->second;
}
void sendNotification(const std::string& method, const nlohmann::json& params) {
nlohmann::json msg;
msg["jsonrpc"] = "2.0";
@@ -68,4 +174,5 @@ private:
std::shared_ptr<LSPTransport> transport_;
int nextId_ = 1;
std::unordered_map<std::string, std::vector<Diagnostic>> diagnosticsByUri_;
};

View File

@@ -24,6 +24,7 @@
#include "WelcomeScreen.h"
#include "DragDropHandler.h"
#include "FileWatcher.h"
#include "LSPClient.h"
#include "ast/Generator.h"
#include <cstdio>
@@ -52,6 +53,7 @@ struct BufferState {
bool modified = false;
int cursorLine = 1;
int cursorCol = 1;
int lspVersion = 1;
std::string editBuf;
std::vector<HighlightSpan> highlights;
bool highlightsDirty = true;
@@ -82,9 +84,49 @@ struct EditorState {
FileNode fileTreeRoot;
bool fileTreeDirty = true;
FileWatcher watcher;
std::shared_ptr<LSPTransport> lspTransport;
std::shared_ptr<LSPClient> lsp;
BufferState* active() { return activeBuffer; }
static std::string toFileUri(const std::string& path) {
std::filesystem::path p(path);
return "file:///" + p.generic_string();
}
static std::string fromFileUri(const std::string& uri) {
const std::string prefix = "file:///";
if (uri.rfind(prefix, 0) != 0) return uri;
std::string path = uri.substr(prefix.size());
std::replace(path.begin(), path.end(), '/', '\\');
return path;
}
static int byteOffsetForLineCol(const std::string& text, int lineZero, int colZero) {
if (lineZero < 0) lineZero = 0;
if (colZero < 0) colZero = 0;
int line = 0;
int pos = 0;
while (pos < (int)text.size() && line < lineZero) {
if (text[pos] == '\n') ++line;
++pos;
}
int col = 0;
while (pos < (int)text.size() && text[pos] != '\n' && col < colZero) {
++pos;
++col;
}
return pos;
}
void jumpTo(BufferState* buf, int lineZero, int colZero) {
if (!buf) return;
int pos = byteOffsetForLineCol(buf->editBuf, lineZero, colZero);
buf->widget.setCursor(pos);
buf->cursorLine = lineZero + 1;
buf->cursorCol = colZero + 1;
}
std::string makeUntitledName() const {
if (!buffers.hasBuffer("(untitled)")) return "(untitled)";
int i = 1;
@@ -111,10 +153,14 @@ struct EditorState {
state->editBuf = content;
state->highlightsDirty = true;
state->modified = false;
state->lspVersion = 1;
buffers.openBuffer(path, content, language);
activeBuffer = state.get();
bufferStates[path] = std::move(state);
if (path.rfind("(untitled", 0) != 0) watcher.watch(path);
if (lsp && path.rfind("(untitled", 0) != 0) {
lsp->didOpen(toFileUri(path), language, content, activeBuffer->lspVersion);
}
}
void switchToBuffer(const std::string& path) {
@@ -186,6 +232,10 @@ struct EditorState {
active()->sync.syncNow();
active()->highlightsDirty = true;
active()->modified = true;
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
active()->lspVersion += 1;
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
}
}
void doUndo() {
@@ -249,6 +299,7 @@ struct EditorState {
out.close();
active()->modified = false;
outputLog += "Saved: " + active()->path + "\n";
if (lsp) lsp->didSave(toFileUri(active()->path), active()->editBuf);
} else {
outputLog += "Error saving: " + active()->path + "\n";
}
@@ -342,6 +393,13 @@ struct EditorState {
}
};
struct NullLSPTransport : public LSPTransport {
void send(const std::string& msg) override { (void)msg; }
bool receive(std::string& out) override { (void)out; return false; }
bool isOpen() const override { return false; }
void close() override {}
};
// ---------------------------------------------------------------------------
// ImGui InputTextMultiline with std::string resize callback
@@ -643,6 +701,8 @@ int main(int, char**) {
// Editor state
EditorState state;
state.init();
state.lspTransport = std::make_shared<NullLSPTransport>();
state.lsp = std::make_shared<LSPClient>(state.lspTransport);
// File dialog defaults
std::string lastDialogPath;
@@ -958,6 +1018,39 @@ int main(int, char**) {
ImGui::EndTabItem();
}
// Problems (LSP diagnostics)
if (ImGui::BeginTabItem("Problems")) {
ImGui::PushFont(monoFont);
ImGui::BeginChild("##problemsScroll", ImVec2(0, 0), false);
auto diags = state.lsp ? state.lsp->getDiagnostics() : std::vector<LSPClient::Diagnostic>{};
if (diags.empty()) {
ImGui::TextDisabled("(no diagnostics)");
} else {
for (const auto& d : diags) {
const char* sev = "Unknown";
if (d.severity == 1) sev = "Error";
else if (d.severity == 2) sev = "Warning";
else if (d.severity == 3) sev = "Info";
else if (d.severity == 4) sev = "Hint";
std::string path = EditorState::fromFileUri(d.uri);
std::string label = "[" + std::string(sev) + "] " + d.message +
" (" + path + ":" + std::to_string(d.range.start.line + 1) +
":" + std::to_string(d.range.start.character + 1) + ")";
if (ImGui::Selectable(label.c_str())) {
if (!path.empty()) {
if (state.buffers.hasBuffer(path)) state.switchToBuffer(path);
else state.doOpen(path);
state.jumpTo(state.active(), d.range.start.line, d.range.start.character);
}
}
}
}
ImGui::EndChild();
ImGui::PopFont();
ImGui::EndTabItem();
}
// AST view
if (ImGui::BeginTabItem("AST")) {
ImGui::PushFont(monoFont);