//===--- Transport.h - Sending and Receiving LSP messages -------*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // The language server protocol is usually implemented by writing messages as // JSON-RPC over the stdin/stdout of a subprocess. This file contains a JSON // transport interface that handles this communication. // //===----------------------------------------------------------------------===// #ifndef LLVM_SUPPORT_LSP_TRANSPORT_H #define LLVM_SUPPORT_LSP_TRANSPORT_H #include "llvm/ADT/FunctionExtras.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Compiler.h" #include "llvm/Support/FormatAdapters.h" #include "llvm/Support/JSON.h" #include "llvm/Support/LSP/Logging.h" #include "llvm/Support/LSP/Protocol.h" #include "llvm/Support/raw_ostream.h" #include namespace llvm { // Simple helper function that returns a string as printed from a op. template static std::string debugString(T &&Op) { std::string InstrStr; llvm::raw_string_ostream Os(InstrStr); Os << Op; return Os.str(); } namespace lsp { class MessageHandler; //===----------------------------------------------------------------------===// // JSONTransport //===----------------------------------------------------------------------===// /// The encoding style of the JSON-RPC messages (both input and output). enum JSONStreamStyle { /// Encoding per the LSP specification, with mandatory Content-Length header. Standard, /// Messages are delimited by a '// -----' line. Comment lines start with //. Delimited }; /// An abstract class used by the JSONTransport to read JSON message. class JSONTransportInput { public: explicit JSONTransportInput(JSONStreamStyle Style = JSONStreamStyle::Standard) : Style(Style) {} virtual ~JSONTransportInput() = default; virtual bool hasError() const = 0; virtual bool isEndOfInput() const = 0; /// Read in a message from the input stream. LogicalResult readMessage(std::string &Json) { return Style == JSONStreamStyle::Delimited ? readDelimitedMessage(Json) : readStandardMessage(Json); } virtual LogicalResult readDelimitedMessage(std::string &Json) = 0; virtual LogicalResult readStandardMessage(std::string &Json) = 0; private: /// The JSON stream style to use. JSONStreamStyle Style; }; /// Concrete implementation of the JSONTransportInput that reads from a file. class JSONTransportInputOverFile : public JSONTransportInput { public: explicit JSONTransportInputOverFile( std::FILE *In, JSONStreamStyle Style = JSONStreamStyle::Standard) : JSONTransportInput(Style), In(In) {} bool hasError() const final { return ferror(In); } bool isEndOfInput() const final { return feof(In); } LLVM_ABI_FOR_TEST LogicalResult readDelimitedMessage(std::string &Json) final; LLVM_ABI_FOR_TEST LogicalResult readStandardMessage(std::string &Json) final; private: std::FILE *In; }; /// A transport class that performs the JSON-RPC communication with the LSP /// client. class JSONTransport { public: JSONTransport(std::unique_ptr In, raw_ostream &Out, bool PrettyOutput = false) : In(std::move(In)), Out(Out), PrettyOutput(PrettyOutput) {} JSONTransport(std::FILE *In, raw_ostream &Out, JSONStreamStyle Style = JSONStreamStyle::Standard, bool PrettyOutput = false) : In(std::make_unique(In, Style)), Out(Out), PrettyOutput(PrettyOutput) {} /// The following methods are used to send a message to the LSP client. LLVM_ABI_FOR_TEST void notify(StringRef Method, llvm::json::Value Params); LLVM_ABI_FOR_TEST void call(StringRef Method, llvm::json::Value Params, llvm::json::Value Id); LLVM_ABI_FOR_TEST void reply(llvm::json::Value Id, llvm::Expected Result); /// Start executing the JSON-RPC transport. LLVM_ABI_FOR_TEST llvm::Error run(MessageHandler &Handler); private: /// Dispatches the given incoming json message to the message handler. bool handleMessage(llvm::json::Value Msg, MessageHandler &Handler); /// Writes the given message to the output stream. void sendMessage(llvm::json::Value Msg); private: /// The input to read a message from. std::unique_ptr In; SmallVector OutputBuffer; /// The output file stream. raw_ostream &Out; /// If the output JSON should be formatted for easier readability. bool PrettyOutput; }; //===----------------------------------------------------------------------===// // MessageHandler //===----------------------------------------------------------------------===// /// A Callback is a void function that accepts Expected. This is /// accepted by functions that logically return T. template using Callback = llvm::unique_function)>; /// An OutgoingNotification is a function used for outgoing notifications /// send to the client. template using OutgoingNotification = llvm::unique_function; /// An OutgoingRequest is a function used for outgoing requests to send to /// the client. template using OutgoingRequest = llvm::unique_function; /// An `OutgoingRequestCallback` is invoked when an outgoing request to the /// client receives a response in turn. It is passed the original request's ID, /// as well as the response result. template using OutgoingRequestCallback = std::function)>; /// A handler used to process the incoming transport messages. class MessageHandler { public: MessageHandler(JSONTransport &Transport) : Transport(Transport) {} bool onNotify(StringRef Method, llvm::json::Value Value); bool onCall(StringRef Method, llvm::json::Value Params, llvm::json::Value Id); bool onReply(llvm::json::Value Id, llvm::Expected Result); template static llvm::Expected parse(const llvm::json::Value &Raw, StringRef PayloadName, StringRef PayloadKind) { T Result; llvm::json::Path::Root Root; if (fromJSON(Raw, Result, Root)) return std::move(Result); // Dump the relevant parts of the broken message. std::string Context; llvm::raw_string_ostream Os(Context); Root.printErrorContext(Raw, Os); // Report the error (e.g. to the client). return llvm::make_error( llvm::formatv("failed to decode {0} {1}: {2}", PayloadName, PayloadKind, fmt_consume(Root.getError())), ErrorCode::InvalidParams); } template void method(llvm::StringLiteral Method, ThisT *ThisPtr, void (ThisT::*Handler)(const Param &, Callback)) { MethodHandlers[Method] = [Method, Handler, ThisPtr](llvm::json::Value RawParams, Callback Reply) { llvm::Expected Parameter = parse(RawParams, Method, "request"); if (!Parameter) return Reply(Parameter.takeError()); (ThisPtr->*Handler)(*Parameter, std::move(Reply)); }; } template void notification(llvm::StringLiteral Method, ThisT *ThisPtr, void (ThisT::*Handler)(const Param &)) { NotificationHandlers[Method] = [Method, Handler, ThisPtr](llvm::json::Value RawParams) { llvm::Expected Parameter = parse(RawParams, Method, "notification"); if (!Parameter) { return llvm::consumeError(llvm::handleErrors( Parameter.takeError(), [](const LSPError &LspError) { Logger::error("JSON parsing error: {0}", LspError.message.c_str()); })); } (ThisPtr->*Handler)(*Parameter); }; } /// Create an OutgoingNotification object used for the given method. template OutgoingNotification outgoingNotification(llvm::StringLiteral Method) { return [&, Method](const T &Params) { std::lock_guard TransportLock(TransportOutputMutex); Logger::info("--> {0}", Method); Transport.notify(Method, llvm::json::Value(Params)); }; } /// Create an OutgoingRequest function that, when called, sends a request with /// the given method via the transport. Should the outgoing request be /// met with a response, the result JSON is parsed and the response callback /// is invoked. template OutgoingRequest outgoingRequest(llvm::StringLiteral Method, OutgoingRequestCallback Callback) { return [&, Method, Callback](const Param &Parameter, llvm::json::Value Id) { auto CallbackWrapper = [Method, Callback = std::move(Callback)]( llvm::json::Value Id, llvm::Expected Value) { if (!Value) return Callback(std::move(Id), Value.takeError()); std::string ResponseName = llvm::formatv("reply:{0}({1})", Method, Id); llvm::Expected ParseResult = parse(*Value, ResponseName, "response"); if (!ParseResult) return Callback(std::move(Id), ParseResult.takeError()); return Callback(std::move(Id), *ParseResult); }; { std::lock_guard Lock(ResponseHandlersMutex); ResponseHandlers.insert( {debugString(Id), std::make_pair(Method.str(), CallbackWrapper)}); } std::lock_guard TransportLock(TransportOutputMutex); Logger::info("--> {0}({1})", Method, Id); Transport.call(Method, llvm::json::Value(Parameter), Id); }; } private: template using HandlerMap = llvm::StringMap>; HandlerMap NotificationHandlers; HandlerMap)> MethodHandlers; /// A pair of (1) the original request's method name, and (2) the callback /// function to be invoked for responses. using ResponseHandlerTy = std::pair>; /// A mapping from request/response ID to response handler. llvm::StringMap ResponseHandlers; /// Mutex to guard insertion into the response handler map. std::mutex ResponseHandlersMutex; JSONTransport &Transport; /// Mutex to guard sending output messages to the transport. std::mutex TransportOutputMutex; }; } // namespace lsp } // namespace llvm #endif