How LspCpp represents LSP messages in C++ and how to extend them.
Request and notification helper macros live in include/LibLsp/JsonRpc/RequestInMessage.h and include/LibLsp/JsonRpc/NotificationInMessage.h. Reflection helpers live in include/LibLsp/JsonRpc/serializer.h.
DEFINE_REQUEST_RESPONSE_TYPE(td_initialize, lsInitializeParams, InitializeResult, "initialize");Expands to:
td_initialize::request— incoming request structtd_initialize::response— outgoing response structtd_initialize::request::kMethodInfo— method name constant ("initialize")- JSON parse/serialize hooks via
ReflectReader/ToJson()
DEFINE_NOTIFICATION_TYPE(Notify_TextDocumentDidOpen, TextDocumentDidOpen::Params, "textDocument/didOpen");Expands to Notify_TextDocumentDidOpen::notify with no response type.
Data fields use RapidJSON reflection macros:
struct InitializeResult {
lsServerCapabilities capabilities;
MAKE_SWAP_METHOD(InitializeResult, capabilities);
};
MAKE_REFLECT_STRUCT(InitializeResult, capabilities);MAKE_REFLECT_STRUCT generates ReflectReader for deserialization and ToJson() for serialization.
A generated request inherits from lsRequest<Params, request>, which inherits from RequestInMessage. Conceptually, it exposes:
struct request : lsRequest<ParamsType, request> {
static constexpr MethodType kMethodInfo = "textDocument/definition";
using Response = response;
// params is inherited from lsRequest<ParamsType, request>
// id and method are inherited from RequestInMessage
// ReflectReader, ToJson, etc.
};Responses inherit from ResponseMessage<ResultType, response> and expose a result plus the inherited JSON-RPC response fields:
struct response : ResponseMessage<InitializeResult, response> {
// result is inherited from ResponseMessage<InitializeResult, response>
// id is inherited from ResponseInMessage
};RemoteEndPoint::registerHandler deduces the method from the handler signature:
// Request: first parameter is td_initialize::request const&
remote_end_point.registerHandler([](td_initialize::request const& req) {
td_initialize::response rsp;
rsp.id = req.id;
return rsp;
});
// Notification: first parameter is Notify_Exit::notify const&
remote_end_point.registerHandler([](Notify_Exit::notify const&) { });Return types:
Torlsp::ResponseOrError<T>for requestsvoidfor notifications
For cancellable requests, the second parameter is CancelMonitor const&.
src/lsp/ProtocolJsonHandler.cpp registers every known LSP method with its JSON parser. When you add a new method:
- Create the header with
DEFINE_*macros. - Include the header from
src/lsp/ProtocolJsonHandler.cpp. - Register the parser in the relevant helper (
AddStandardRequestJsonRpcMethod,AddNotifyJsonRpcMethod, or one of the response helpers).
At runtime, StreamMessageProducer looks up the method string in this handler to deserialize params before dispatch.
| Prefix | Meaning | Example |
|---|---|---|
td_ |
textDocument request | td_hover, td_completion |
Notify_ |
Notification | Notify_TextDocumentDidOpen |
ls prefix on structs |
LSP data types | lsPosition, lsRange, lsDiagnostic |
Method struct names abbreviate the LSP path:
textDocument/definition→td_definitiontextDocument/didOpen→Notify_TextDocumentDidOpen
Search existing headers under include/LibLsp/lsp/ before inventing new names.
LspCpp tracks multiple LSP versions:
| Test file | Coverage |
|---|---|
lsp_types_roundtrip_tests.cpp |
Core types, general round-trips |
lsp_3_16_17_tests.cpp |
LSP 3.16 and 3.17 additions |
lsp_3_18_tests.cpp |
LSP 3.18 additions |
Version-specific headers include protocol_3_18.h and feature-gated structs. When adding types for a new spec version, place them in a appropriately named header and extend the matching test file.
LSP optional fields use helper types from the codebase:
optional<T>wrappers (seeoptionalVersion.h; this aliasesstd::optionalin C++17 builds)lsp::Anyfor loosely typed JSON values
Follow existing structs in the same header for nullable vs. omitted field semantics.
If the default ReflectReader is insufficient:
session.overrideRequestParser(
"myExtension/doThing",
[](Reader& visitor) -> std::unique_ptr<LspMessage> {
// custom parse logic
});Or override the typed parser:
session.overrideRequestParser<td_initialize::request>();Use sparingly—prefer fixing the struct reflection when the JSON shape matches the spec.
Return errors with Rsp_Error:
Rsp_Error err;
err.id = req.id;
err.error.code = lsErrorCodes::InternalError;
err.error.message = "something went wrong";
return err;Or use lsp::ResponseOrError<T>:
return lsp::ResponseOrError<td_foo::response>(err);Standard error codes are in lsErrorCodes (see LSP spec appendix).
The include/LibLsp/lsp/extention/jdtls/ directory contains Eclipse JDT Language Server extensions retained for compatibility with Java tooling built on LspCpp. These are not part of the standard LSP spec.
Add round-trip tests that serialize a struct to JSON and parse it back:
// Pattern used in lsp_types_roundtrip_tests.cpp
auto json = value.ToJson();
Reader reader(json);
auto parsed = SomeType::ReflectReader(reader);
Expect(parsed.field == value.field, "field must round-trip");This catches reflection macro mistakes early.
- LSP specification
- Architecture — where parsing and dispatch fit in the stack
- Writing a language server — using types from application code