#pragma once // Step 90: LSP client core // // Minimal JSON-RPC LSP client with injectable transport. #include #include #include #include #include #include class LSPTransport { public: virtual ~LSPTransport() = default; virtual void send(const std::string& msg) = 0; virtual bool receive(std::string& out) = 0; virtual bool isOpen() const = 0; virtual void close() = 0; }; class LSPClient { public: explicit LSPClient(std::shared_ptr 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; }; struct CompletionItem { std::string label; int kind = 0; std::string detail; std::string insertText; std::string filterText; }; struct SignatureHelp { std::string label; int activeSignature = 0; int activeParameter = 0; }; struct DefinitionLocation { std::string uri; Range range; }; struct DocumentSymbol { std::string name; std::string detail; int kind = 0; Range range; Range selectionRange; std::vector children; }; struct WorkspaceSymbol { std::string name; std::string containerName; std::string detail; int kind = 0; }; void initialize(const std::string& rootUri, const std::string& clientName, int processId) { nlohmann::json params; params["processId"] = processId; params["rootUri"] = rootUri; params["clientInfo"] = { {"name", clientName}, {"version", "0.1"} }; params["capabilities"] = nlohmann::json::object(); sendRequest("initialize", params); sendNotification("initialized", nlohmann::json::object()); } void shutdown() { sendRequest("shutdown", nlohmann::json::object()); sendNotification("exit", nlohmann::json::object()); 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); } int requestCompletion(const std::string& uri, int line, int character) { nlohmann::json params; params["textDocument"] = {{"uri", uri}}; params["position"] = {{"line", line}, {"character", character}}; int id = sendRequest("textDocument/completion", params); lastCompletionRequestId_ = id; return id; } int requestHover(const std::string& uri, int line, int character) { nlohmann::json params; params["textDocument"] = {{"uri", uri}}; params["position"] = {{"line", line}, {"character", character}}; int id = sendRequest("textDocument/hover", params); lastHoverRequestId_ = id; return id; } int requestSignatureHelp(const std::string& uri, int line, int character) { nlohmann::json params; params["textDocument"] = {{"uri", uri}}; params["position"] = {{"line", line}, {"character", character}}; int id = sendRequest("textDocument/signatureHelp", params); lastSignatureRequestId_ = id; return id; } int requestDefinition(const std::string& uri, int line, int character) { nlohmann::json params; params["textDocument"] = {{"uri", uri}}; params["position"] = {{"line", line}, {"character", character}}; int id = sendRequest("textDocument/definition", params); lastDefinitionRequestId_ = id; return id; } int requestDocumentSymbols(const std::string& uri) { nlohmann::json params; params["textDocument"] = {{"uri", uri}}; int id = sendRequest("textDocument/documentSymbol", params); lastDocumentSymbolRequestId_ = id; return id; } int requestWorkspaceSymbols(const std::string& query) { nlohmann::json params; params["query"] = query; int id = sendRequest("workspace/symbol", params); workspaceSymbolRequestIds_.insert(id); return id; } int requestLibraryCompletion(const std::string& uri, int line, int character, const std::string& prefixHint) { nlohmann::json params; params["textDocument"] = {{"uri", uri}}; params["position"] = {{"line", line}, {"character", character}}; params["context"] = {{"triggerKind", 2}, {"triggerCharacter", "."}}; params["whetstonePrefix"] = prefixHint; // STUB: non-standard hint int id = sendRequest("textDocument/completion", params); libraryCompletionRequestIds_.insert(id); return id; } void handleMessage(const std::string& msg) { nlohmann::json j; try { j = nlohmann::json::parse(msg); } catch (...) { return; } if (j.contains("method")) { if (j["method"].get() != "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::vector 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); return; } if (j.contains("id") && j.contains("result")) { int id = j["id"].get(); const auto& result = j["result"]; if (id == lastCompletionRequestId_) { const nlohmann::json* items = nullptr; if (result.is_array()) { items = &result; } else if (result.is_object() && result.contains("items")) { items = &result["items"]; } if (!items || !items->is_array()) return; std::vector parsed; for (const auto& item : *items) { CompletionItem ci; ci.label = item.value("label", ""); ci.kind = item.value("kind", 0); ci.detail = item.value("detail", ""); ci.insertText = item.value("insertText", ci.label); ci.filterText = item.value("filterText", ""); parsed.push_back(std::move(ci)); } completionItems_ = std::move(parsed); return; } if (id == lastHoverRequestId_) { if (result.contains("contents")) { hoverContents_ = stringifyMarkup(result["contents"]); } return; } if (id == lastSignatureRequestId_) { if (result.contains("signatures") && result["signatures"].is_array() && !result["signatures"].empty()) { const auto& sig = result["signatures"][0]; signatureHelp_.label = sig.value("label", ""); signatureHelp_.activeSignature = result.value("activeSignature", 0); signatureHelp_.activeParameter = result.value("activeParameter", 0); } return; } if (id == lastDefinitionRequestId_) { std::vector parsed; if (result.is_array()) { for (const auto& item : result) { DefinitionLocation loc; if (parseDefinitionLocation(item, loc)) parsed.push_back(std::move(loc)); } } else if (result.is_object()) { DefinitionLocation loc; if (parseDefinitionLocation(result, loc)) parsed.push_back(std::move(loc)); } definitionLocations_ = std::move(parsed); return; } if (id == lastDocumentSymbolRequestId_) { std::vector parsed; if (result.is_array()) { bool isSymbolInfo = !result.empty() && result[0].contains("location"); for (const auto& item : result) { DocumentSymbol sym; bool ok = isSymbolInfo ? parseSymbolInformation(item, sym) : parseDocumentSymbol(item, sym); if (ok) parsed.push_back(std::move(sym)); } } else if (result.is_object()) { DocumentSymbol sym; if (parseDocumentSymbol(result, sym)) parsed.push_back(std::move(sym)); } documentSymbols_ = std::move(parsed); return; } if (workspaceSymbolRequestIds_.count(id)) { std::vector parsed; if (result.is_array()) { for (const auto& item : result) { WorkspaceSymbol sym; if (parseWorkspaceSymbol(item, sym)) parsed.push_back(std::move(sym)); } } workspaceSymbolsByRequestId_[id] = std::move(parsed); workspaceSymbolRequestIds_.erase(id); return; } if (libraryCompletionRequestIds_.count(id)) { const nlohmann::json* items = nullptr; if (result.is_array()) { items = &result; } else if (result.is_object() && result.contains("items")) { items = &result["items"]; } if (items && items->is_array()) { std::vector parsed; for (const auto& item : *items) { CompletionItem ci; ci.label = item.value("label", ""); ci.kind = item.value("kind", 0); ci.detail = item.value("detail", ""); ci.insertText = item.value("insertText", ci.label); ci.filterText = item.value("filterText", ""); parsed.push_back(std::move(ci)); } libraryCompletionsByRequestId_[id] = std::move(parsed); } libraryCompletionRequestIds_.erase(id); return; } } } std::vector getDiagnostics() const { std::vector out; for (const auto& kv : diagnosticsByUri_) { out.insert(out.end(), kv.second.begin(), kv.second.end()); } return out; } std::vector getDiagnosticsForUri(const std::string& uri) const { auto it = diagnosticsByUri_.find(uri); if (it == diagnosticsByUri_.end()) return {}; return it->second; } std::vector getCompletionItems() const { return completionItems_; } void clearCompletionItems() { completionItems_.clear(); } std::string getHoverContents() const { return hoverContents_; } void clearHover() { hoverContents_.clear(); } SignatureHelp getSignatureHelp() const { return signatureHelp_; } void clearSignatureHelp() { signatureHelp_ = SignatureHelp{}; } std::vector getDefinitionLocations() const { return definitionLocations_; } void clearDefinitionLocations() { definitionLocations_.clear(); } std::vector getDocumentSymbols() const { return documentSymbols_; } void clearDocumentSymbols() { documentSymbols_.clear(); } bool takeWorkspaceSymbols(int requestId, std::vector& out) { auto it = workspaceSymbolsByRequestId_.find(requestId); if (it == workspaceSymbolsByRequestId_.end()) return false; out = std::move(it->second); workspaceSymbolsByRequestId_.erase(it); return true; } bool takeLibraryCompletions(int requestId, std::vector& out) { auto it = libraryCompletionsByRequestId_.find(requestId); if (it == libraryCompletionsByRequestId_.end()) return false; out = std::move(it->second); libraryCompletionsByRequestId_.erase(it); return true; } void sendNotification(const std::string& method, const nlohmann::json& params) { nlohmann::json msg; msg["jsonrpc"] = "2.0"; msg["method"] = method; msg["params"] = params; sendMessage(msg); } int sendRequest(const std::string& method, const nlohmann::json& params) { nlohmann::json msg; msg["jsonrpc"] = "2.0"; msg["id"] = nextId_++; msg["method"] = method; msg["params"] = params; sendMessage(msg); return msg["id"].get(); } private: void sendMessage(const nlohmann::json& msg) { if (!transport_) return; std::string body = msg.dump(); std::string header = "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n"; transport_->send(header + body); } std::shared_ptr transport_; int nextId_ = 1; std::unordered_map> diagnosticsByUri_; int lastCompletionRequestId_ = -1; std::vector completionItems_; int lastHoverRequestId_ = -1; int lastSignatureRequestId_ = -1; int lastDefinitionRequestId_ = -1; int lastDocumentSymbolRequestId_ = -1; std::string hoverContents_; SignatureHelp signatureHelp_; std::vector definitionLocations_; std::vector documentSymbols_; std::unordered_set workspaceSymbolRequestIds_; std::unordered_map> workspaceSymbolsByRequestId_; std::unordered_set libraryCompletionRequestIds_; std::unordered_map> libraryCompletionsByRequestId_; static Range parseRange(const nlohmann::json& range) { Range out; if (range.contains("start")) { const auto& start = range["start"]; out.start.line = start.value("line", 0); out.start.character = start.value("character", 0); } if (range.contains("end")) { const auto& end = range["end"]; out.end.line = end.value("line", 0); out.end.character = end.value("character", 0); } return out; } static bool parseDefinitionLocation(const nlohmann::json& item, DefinitionLocation& out) { if (item.contains("uri") && item.contains("range")) { out.uri = item.value("uri", ""); out.range = parseRange(item["range"]); return !out.uri.empty(); } if (item.contains("targetUri")) { out.uri = item.value("targetUri", ""); if (item.contains("targetRange")) { out.range = parseRange(item["targetRange"]); } else if (item.contains("targetSelectionRange")) { out.range = parseRange(item["targetSelectionRange"]); } return !out.uri.empty(); } return false; } static bool parseDocumentSymbol(const nlohmann::json& item, DocumentSymbol& out) { if (!item.contains("name")) return false; out.name = item.value("name", ""); out.detail = item.value("detail", ""); out.kind = item.value("kind", 0); if (item.contains("range")) out.range = parseRange(item["range"]); if (item.contains("selectionRange")) { out.selectionRange = parseRange(item["selectionRange"]); } else { out.selectionRange = out.range; } if (item.contains("children") && item["children"].is_array()) { for (const auto& child : item["children"]) { DocumentSymbol c; if (parseDocumentSymbol(child, c)) out.children.push_back(std::move(c)); } } return !out.name.empty(); } static bool parseSymbolInformation(const nlohmann::json& item, DocumentSymbol& out) { if (!item.contains("name") || !item.contains("location")) return false; out.name = item.value("name", ""); out.detail = item.value("detail", ""); out.kind = item.value("kind", 0); const auto& loc = item["location"]; if (loc.contains("range")) { out.range = parseRange(loc["range"]); out.selectionRange = out.range; } return !out.name.empty(); } static bool parseWorkspaceSymbol(const nlohmann::json& item, WorkspaceSymbol& out) { if (!item.contains("name")) return false; out.name = item.value("name", ""); out.detail = item.value("detail", ""); out.kind = item.value("kind", 0); out.containerName = item.value("containerName", ""); return !out.name.empty(); } static std::string stringifyMarkup(const nlohmann::json& contents) { if (contents.is_string()) return contents.get(); if (contents.is_object()) { if (contents.contains("value")) return contents["value"].get(); } if (contents.is_array()) { std::string out; for (const auto& item : contents) { if (!out.empty()) out += "\n"; out += stringifyMarkup(item); } return out; } return ""; } };