diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -14,6 +14,7 @@ #include "Features.inc" #include "FindSymbols.h" #include "GlobalCompilationDatabase.h" +#include "LSPBinder.h" #include "Protocol.h" #include "Transport.h" #include "support/Context.h" @@ -90,8 +91,8 @@ // Calls have signature void(const Params&, Callback). void onInitialize(const InitializeParams &, Callback); void onInitialized(const InitializedParams &); - void onShutdown(Callback); - void onSync(Callback); + void onShutdown(const NoParams &, Callback); + void onSync(const NoParams &, Callback); void onDocumentDidOpen(const DidOpenTextDocumentParams &); void onDocumentDidChange(const DidChangeTextDocumentParams &); void onDocumentDidClose(const DidCloseTextDocumentParams &); @@ -157,11 +158,7 @@ Callback); /// This is a clangd extension. Provides a json tree representing memory usage /// hierarchy. - void onMemoryUsage(Callback); - - llvm::StringMap)>> - CommandHandlers; + void onMemoryUsage(const NoParams &, Callback); void onCommand(const ExecuteCommandParams &, Callback); /// Implement commands. @@ -229,29 +226,6 @@ std::unique_ptr MsgHandler; std::mutex TranspWriter; - template - static Expected parse(const llvm::json::Value &Raw, - llvm::StringRef PayloadName, - llvm::StringRef PayloadKind) { - T Result; - llvm::json::Path::Root Root; - if (!fromJSON(Raw, Result, Root)) { - elog("Failed to decode {0} {1}: {2}", PayloadName, PayloadKind, - Root.getError()); - // Dump the relevant parts of the broken message. - std::string Context; - llvm::raw_string_ostream OS(Context); - Root.printErrorContext(Raw, OS); - vlog("{0}", OS.str()); - // 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); - } - return std::move(Result); - } - template void call(StringRef Method, llvm::json::Value Params, Callback CB) { // Wrap the callback with LSP conversion and error-handling. @@ -261,7 +235,7 @@ llvm::Expected RawResponse) mutable { if (!RawResponse) return CB(RawResponse.takeError()); - CB(parse(*RawResponse, Method, "response")); + CB(LSPBinder::parse(*RawResponse, Method, "response")); }; callRaw(Method, std::move(Params), std::move(HandleReply)); } @@ -274,19 +248,8 @@ Params.value = std::move(Value); notify("$/progress", Params); } - template - void bindCommand(llvm::StringLiteral Method, - void (ClangdLSPServer::*Handler)(const Param &, - Callback)) { - CommandHandlers[Method] = [Method, Handler, - this](llvm::json::Value RawParams, - Callback Reply) { - auto P = parse(RawParams, Method, "command"); - if (!P) - return Reply(P.takeError()); - (this->*Handler)(*P, std::move(Reply)); - }; - } + + LSPBinder::RawHandlers Handlers; const ThreadsafeFS &TFS; /// Options used for diagnostics. diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -13,6 +13,7 @@ #include "DraftStore.h" #include "DumpAST.h" #include "GlobalCompilationDatabase.h" +#include "LSPBinder.h" #include "Protocol.h" #include "SemanticHighlighting.h" #include "SourceCode.h" @@ -158,6 +159,8 @@ MessageHandler(ClangdLSPServer &Server) : Server(Server) {} bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override { + trace::Span Tracer(Method, LSPLatency); + SPAN_ATTACH(Tracer, "Params", Params); WithContext HandlerContext(handlerContext()); log("<-- {0}", Method); if (Method == "exit") @@ -166,12 +169,15 @@ elog("Notification {0} before initialization", Method); } else if (Method == "$/cancelRequest") { onCancel(std::move(Params)); - } else if (auto Handler = Notifications.lookup(Method)) { - Handler(std::move(Params)); - Server.maybeExportMemoryProfile(); - Server.maybeCleanupMemory(); } else { - log("unhandled notification {0}", Method); + auto Handler = Server.Handlers.NotificationHandlers.find(Method); + if (Handler != Server.Handlers.NotificationHandlers.end()) { + Handler->second(std::move(Params)); + Server.maybeExportMemoryProfile(); + Server.maybeCleanupMemory(); + } else { + log("unhandled notification {0}", Method); + } } return true; } @@ -189,11 +195,15 @@ elog("Call {0} before initialization.", Method); Reply(llvm::make_error("server not initialized", ErrorCode::ServerNotInitialized)); - } else if (auto Handler = Calls.lookup(Method)) - Handler(std::move(Params), std::move(Reply)); - else - Reply(llvm::make_error("method not found", - ErrorCode::MethodNotFound)); + } else { + auto Handler = Server.Handlers.MethodHandlers.find(Method); + if (Handler != Server.Handlers.MethodHandlers.end()) { + Handler->second(std::move(Params), std::move(Reply)); + } else { + Reply(llvm::make_error("method not found", + ErrorCode::MethodNotFound)); + } + } return true; } @@ -236,28 +246,6 @@ return true; } - // Bind an LSP method name to a call. - template - void bind(const char *Method, - void (ClangdLSPServer::*Handler)(const Param &, Callback)) { - Calls[Method] = [Method, Handler, this](llvm::json::Value RawParams, - ReplyOnce Reply) { - auto P = parse(RawParams, Method, "request"); - if (!P) - return Reply(P.takeError()); - (Server.*Handler)(*P, std::move(Reply)); - }; - } - - template - void bind(const char *Method, - void (ClangdLSPServer::*Handler)(Callback)) { - Calls[Method] = [Handler, this](llvm::json::Value RawParams, - ReplyOnce Reply) { - (Server.*Handler)(std::move(Reply)); - }; - } - // Bind a reply callback to a request. The callback will be invoked when // clangd receives the reply from the LSP client. // Return a call id of the request. @@ -286,27 +274,6 @@ return ID; } - // Bind an LSP method name to a notification. - template - void bind(const char *Method, - void (ClangdLSPServer::*Handler)(const Param &)) { - Notifications[Method] = [Method, Handler, - this](llvm::json::Value RawParams) { - llvm::Expected P = parse(RawParams, Method, "request"); - if (!P) - return llvm::consumeError(P.takeError()); - trace::Span Tracer(Method, LSPLatency); - SPAN_ATTACH(Tracer, "Params", RawParams); - (Server.*Handler)(*P); - }; - } - - void bind(const char *Method, void (ClangdLSPServer::*Handler)()) { - Notifications[Method] = [Handler, this](llvm::json::Value RawParams) { - (Server.*Handler)(); - }; - } - private: // Function object to reply to an LSP call. // Each instance must be called exactly once, otherwise: @@ -378,9 +345,6 @@ } }; - llvm::StringMap> Notifications; - llvm::StringMap> Calls; - // Method calls may be cancelled by ID, so keep track of their state. // This needs a mutex: handlers may finish on a different thread, and that's // when we clean up entries in the map. @@ -449,14 +413,6 @@ }; constexpr int ClangdLSPServer::MessageHandler::MaxReplayCallbacks; -template <> -void ClangdLSPServer::MessageHandler::bind( - const char *Method, void (ClangdLSPServer::*Handler)(const NoParams &)) { - Notifications[Method] = [Handler, this](llvm::json::Value RawParams) { - (Server.*Handler)(NoParams{}); - }; -} - // call(), notify(), and reply() wrap the Transport, adding logging and locking. void ClangdLSPServer::callRaw(StringRef Method, llvm::json::Value Params, Callback CB) { @@ -586,7 +542,7 @@ CodeAction::INFO_KIND}}}; std::vector Commands; - for (llvm::StringRef Command : CommandHandlers.keys()) + for (llvm::StringRef Command : Handlers.CommandHandlers.keys()) Commands.push_back(Command); llvm::sort(Commands); @@ -665,7 +621,8 @@ void ClangdLSPServer::onInitialized(const InitializedParams &Params) {} -void ClangdLSPServer::onShutdown(Callback Reply) { +void ClangdLSPServer::onShutdown(const NoParams &, + Callback Reply) { // Do essentially nothing, just say we're ready to exit. ShutdownRequestReceived = true; Reply(nullptr); @@ -673,7 +630,7 @@ // sync is a clangd extension: it blocks until all background work completes. // It blocks the calling thread, so no messages are processed until it returns! -void ClangdLSPServer::onSync(Callback Reply) { +void ClangdLSPServer::onSync(const NoParams &, Callback Reply) { if (Server->blockUntilIdleForTest(/*TimeoutSeconds=*/60)) Reply(nullptr); else @@ -734,8 +691,8 @@ void ClangdLSPServer::onCommand(const ExecuteCommandParams &Params, Callback Reply) { - auto It = CommandHandlers.find(Params.command); - if (It == CommandHandlers.end()) { + auto It = Handlers.CommandHandlers.find(Params.command); + if (It == Handlers.CommandHandlers.end()) { return Reply(llvm::make_error( llvm::formatv("Unsupported command \"{0}\".", Params.command).str(), ErrorCode::InvalidParams)); @@ -1462,7 +1419,8 @@ }); } -void ClangdLSPServer::onMemoryUsage(Callback Reply) { +void ClangdLSPServer::onMemoryUsage(const NoParams &, + Callback Reply) { llvm::BumpPtrAllocator DetailAlloc; MemoryTree MT(&DetailAlloc); profile(MT); @@ -1493,50 +1451,51 @@ } // clang-format off - MsgHandler->bind("initialize", &ClangdLSPServer::onInitialize); - MsgHandler->bind("initialized", &ClangdLSPServer::onInitialized); - MsgHandler->bind("shutdown", &ClangdLSPServer::onShutdown); - MsgHandler->bind("sync", &ClangdLSPServer::onSync); - MsgHandler->bind("textDocument/rangeFormatting", &ClangdLSPServer::onDocumentRangeFormatting); - MsgHandler->bind("textDocument/onTypeFormatting", &ClangdLSPServer::onDocumentOnTypeFormatting); - MsgHandler->bind("textDocument/formatting", &ClangdLSPServer::onDocumentFormatting); - MsgHandler->bind("textDocument/codeAction", &ClangdLSPServer::onCodeAction); - MsgHandler->bind("textDocument/completion", &ClangdLSPServer::onCompletion); - MsgHandler->bind("textDocument/signatureHelp", &ClangdLSPServer::onSignatureHelp); - MsgHandler->bind("textDocument/definition", &ClangdLSPServer::onGoToDefinition); - MsgHandler->bind("textDocument/declaration", &ClangdLSPServer::onGoToDeclaration); - MsgHandler->bind("textDocument/implementation", &ClangdLSPServer::onGoToImplementation); - MsgHandler->bind("textDocument/references", &ClangdLSPServer::onReference); - MsgHandler->bind("textDocument/switchSourceHeader", &ClangdLSPServer::onSwitchSourceHeader); - MsgHandler->bind("textDocument/prepareRename", &ClangdLSPServer::onPrepareRename); - MsgHandler->bind("textDocument/rename", &ClangdLSPServer::onRename); - MsgHandler->bind("textDocument/hover", &ClangdLSPServer::onHover); - MsgHandler->bind("textDocument/documentSymbol", &ClangdLSPServer::onDocumentSymbol); - MsgHandler->bind("workspace/executeCommand", &ClangdLSPServer::onCommand); - MsgHandler->bind("textDocument/documentHighlight", &ClangdLSPServer::onDocumentHighlight); - MsgHandler->bind("workspace/symbol", &ClangdLSPServer::onWorkspaceSymbol); - MsgHandler->bind("textDocument/ast", &ClangdLSPServer::onAST); - MsgHandler->bind("textDocument/didOpen", &ClangdLSPServer::onDocumentDidOpen); - MsgHandler->bind("textDocument/didClose", &ClangdLSPServer::onDocumentDidClose); - MsgHandler->bind("textDocument/didChange", &ClangdLSPServer::onDocumentDidChange); - MsgHandler->bind("textDocument/didSave", &ClangdLSPServer::onDocumentDidSave); - MsgHandler->bind("workspace/didChangeWatchedFiles", &ClangdLSPServer::onFileEvent); - MsgHandler->bind("workspace/didChangeConfiguration", &ClangdLSPServer::onChangeConfiguration); - MsgHandler->bind("textDocument/symbolInfo", &ClangdLSPServer::onSymbolInfo); - MsgHandler->bind("textDocument/typeHierarchy", &ClangdLSPServer::onTypeHierarchy); - MsgHandler->bind("typeHierarchy/resolve", &ClangdLSPServer::onResolveTypeHierarchy); - MsgHandler->bind("textDocument/prepareCallHierarchy", &ClangdLSPServer::onPrepareCallHierarchy); - MsgHandler->bind("callHierarchy/incomingCalls", &ClangdLSPServer::onCallHierarchyIncomingCalls); - MsgHandler->bind("callHierarchy/outgoingCalls", &ClangdLSPServer::onCallHierarchyOutgoingCalls); - MsgHandler->bind("textDocument/selectionRange", &ClangdLSPServer::onSelectionRange); - MsgHandler->bind("textDocument/documentLink", &ClangdLSPServer::onDocumentLink); - MsgHandler->bind("textDocument/semanticTokens/full", &ClangdLSPServer::onSemanticTokens); - MsgHandler->bind("textDocument/semanticTokens/full/delta", &ClangdLSPServer::onSemanticTokensDelta); - MsgHandler->bind("$/memoryUsage", &ClangdLSPServer::onMemoryUsage); + LSPBinder Bind(this->Handlers); + Bind.method("initialize", this, &ClangdLSPServer::onInitialize); + Bind.notification("initialized", this, &ClangdLSPServer::onInitialized); + Bind.method("shutdown", this, &ClangdLSPServer::onShutdown); + Bind.method("sync", this, &ClangdLSPServer::onSync); + Bind.method("textDocument/rangeFormatting", this, &ClangdLSPServer::onDocumentRangeFormatting); + Bind.method("textDocument/onTypeFormatting", this, &ClangdLSPServer::onDocumentOnTypeFormatting); + Bind.method("textDocument/formatting", this, &ClangdLSPServer::onDocumentFormatting); + Bind.method("textDocument/codeAction", this, &ClangdLSPServer::onCodeAction); + Bind.method("textDocument/completion", this, &ClangdLSPServer::onCompletion); + Bind.method("textDocument/signatureHelp", this, &ClangdLSPServer::onSignatureHelp); + Bind.method("textDocument/definition", this, &ClangdLSPServer::onGoToDefinition); + Bind.method("textDocument/declaration", this, &ClangdLSPServer::onGoToDeclaration); + Bind.method("textDocument/implementation", this, &ClangdLSPServer::onGoToImplementation); + Bind.method("textDocument/references", this, &ClangdLSPServer::onReference); + Bind.method("textDocument/switchSourceHeader", this, &ClangdLSPServer::onSwitchSourceHeader); + Bind.method("textDocument/prepareRename", this, &ClangdLSPServer::onPrepareRename); + Bind.method("textDocument/rename", this, &ClangdLSPServer::onRename); + Bind.method("textDocument/hover", this, &ClangdLSPServer::onHover); + Bind.method("textDocument/documentSymbol", this, &ClangdLSPServer::onDocumentSymbol); + Bind.method("workspace/executeCommand", this, &ClangdLSPServer::onCommand); + Bind.method("textDocument/documentHighlight", this, &ClangdLSPServer::onDocumentHighlight); + Bind.method("workspace/symbol", this, &ClangdLSPServer::onWorkspaceSymbol); + Bind.method("textDocument/ast", this, &ClangdLSPServer::onAST); + Bind.notification("textDocument/didOpen", this, &ClangdLSPServer::onDocumentDidOpen); + Bind.notification("textDocument/didClose", this, &ClangdLSPServer::onDocumentDidClose); + Bind.notification("textDocument/didChange", this, &ClangdLSPServer::onDocumentDidChange); + Bind.notification("textDocument/didSave", this, &ClangdLSPServer::onDocumentDidSave); + Bind.notification("workspace/didChangeWatchedFiles", this, &ClangdLSPServer::onFileEvent); + Bind.notification("workspace/didChangeConfiguration", this, &ClangdLSPServer::onChangeConfiguration); + Bind.method("textDocument/symbolInfo", this, &ClangdLSPServer::onSymbolInfo); + Bind.method("textDocument/typeHierarchy", this, &ClangdLSPServer::onTypeHierarchy); + Bind.method("typeHierarchy/resolve", this, &ClangdLSPServer::onResolveTypeHierarchy); + Bind.method("textDocument/prepareCallHierarchy", this, &ClangdLSPServer::onPrepareCallHierarchy); + Bind.method("callHierarchy/incomingCalls", this, &ClangdLSPServer::onCallHierarchyIncomingCalls); + Bind.method("callHierarchy/outgoingCalls", this, &ClangdLSPServer::onCallHierarchyOutgoingCalls); + Bind.method("textDocument/selectionRange", this, &ClangdLSPServer::onSelectionRange); + Bind.method("textDocument/documentLink", this, &ClangdLSPServer::onDocumentLink); + Bind.method("textDocument/semanticTokens/full", this, &ClangdLSPServer::onSemanticTokens); + Bind.method("textDocument/semanticTokens/full/delta", this, &ClangdLSPServer::onSemanticTokensDelta); + Bind.method("$/memoryUsage", this, &ClangdLSPServer::onMemoryUsage); if (Opts.FoldingRanges) - MsgHandler->bind("textDocument/foldingRange", &ClangdLSPServer::onFoldingRange); - bindCommand(APPLY_FIX_COMMAND, &ClangdLSPServer::onCommandApplyEdit); - bindCommand(APPLY_TWEAK_COMMAND, &ClangdLSPServer::onCommandApplyTweak); + Bind.method("textDocument/foldingRange", this, &ClangdLSPServer::onFoldingRange); + Bind.command(APPLY_FIX_COMMAND, this, &ClangdLSPServer::onCommandApplyEdit); + Bind.command(APPLY_TWEAK_COMMAND, this, &ClangdLSPServer::onCommandApplyTweak); // clang-format on } diff --git a/clang-tools-extra/clangd/LSPBinder.h b/clang-tools-extra/clangd/LSPBinder.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/LSPBinder.h @@ -0,0 +1,147 @@ +//===--- LSPBinder.h - Tables of LSP handlers --------------------*- 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_LSPBINDER_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_LSPBINDER_H + +#include "Protocol.h" +#include "support/Function.h" +#include "support/Logger.h" +#include "llvm/ADT/FunctionExtras.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/JSON.h" + +namespace clang { +namespace clangd { + +/// LSPBinder collects a table of functions that handle LSP calls. +/// +/// It translates a handler method's signature, e.g. +/// void codeComplete(CompletionParams, Callback) +/// into a wrapper with a generic signature: +/// void(json::Value, Callback) +/// The wrapper takes care of parsing/serializing responses. +/// +/// Incoming calls can be methods, notifications, or commands - all are similar. +/// +/// FIXME: this should also take responsibility for wrapping *outgoing* calls, +/// replacing the typed ClangdLSPServer::call<> etc. +class LSPBinder { +public: + using JSON = llvm::json::Value; + + struct RawHandlers { + template + using HandlerMap = llvm::StringMap>; + + HandlerMap NotificationHandlers; + HandlerMap)> MethodHandlers; + HandlerMap)> CommandHandlers; + }; + + LSPBinder(RawHandlers &Raw) : Raw(Raw) {} + + /// Bind a handler for an LSP method. + /// e.g. Bind.method("peek", this, &ThisModule::peek); + /// Handler should be e.g. void peek(const PeekParams&, Callback); + /// PeekParams must be JSON-parseable and PeekResult must be serializable. + template + void method(llvm::StringLiteral Method, ThisT *This, + void (ThisT::*Handler)(const Param &, Callback)); + + /// Bind a handler for an LSP notification. + /// e.g. Bind.notification("poke", this, &ThisModule::poke); + /// Handler should be e.g. void poke(const PokeParams&); + /// PokeParams must be JSON-parseable. + template + void notification(llvm::StringLiteral Method, ThisT *This, + void (ThisT::*Handler)(const Param &)); + + /// Bind a handler for an LSP command. + /// e.g. Bind.command("load", this, &ThisModule::load); + /// Handler should be e.g. void load(const LoadParams&, Callback); + /// LoadParams must be JSON-parseable and LoadResult must be serializable. + template + void command(llvm::StringLiteral Command, ThisT *This, + void (ThisT::*Handler)(const Param &, Callback)); + + // FIXME: remove usage from ClangdLSPServer and make this private. + template + static llvm::Expected parse(const llvm::json::Value &Raw, + llvm::StringRef PayloadName, + llvm::StringRef PayloadKind); + +private: + RawHandlers &Raw; +}; + +template +llvm::Expected LSPBinder::parse(const llvm::json::Value &Raw, + llvm::StringRef PayloadName, + llvm::StringRef PayloadKind) { + T Result; + llvm::json::Path::Root Root; + if (!fromJSON(Raw, Result, Root)) { + elog("Failed to decode {0} {1}: {2}", PayloadName, PayloadKind, + Root.getError()); + // Dump the relevant parts of the broken message. + std::string Context; + llvm::raw_string_ostream OS(Context); + Root.printErrorContext(Raw, OS); + vlog("{0}", OS.str()); + // 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); + } + return std::move(Result); +} + +template +void LSPBinder::method(llvm::StringLiteral Method, ThisT *This, + void (ThisT::*Handler)(const Param &, + Callback)) { + Raw.MethodHandlers[Method] = [Method, Handler, This](JSON RawParams, + Callback Reply) { + auto P = LSPBinder::parse(RawParams, Method, "request"); + if (!P) + return Reply(P.takeError()); + (This->*Handler)(*P, std::move(Reply)); + }; +} + +template +void LSPBinder::notification(llvm::StringLiteral Method, ThisT *This, + void (ThisT::*Handler)(const Param &)) { + Raw.NotificationHandlers[Method] = [Method, Handler, This](JSON RawParams) { + llvm::Expected P = + LSPBinder::parse(RawParams, Method, "request"); + if (!P) + return llvm::consumeError(P.takeError()); + (This->*Handler)(*P); + }; +} + +template +void LSPBinder::command(llvm::StringLiteral Method, ThisT *This, + void (ThisT::*Handler)(const Param &, + Callback)) { + Raw.CommandHandlers[Method] = [Method, Handler, This](JSON RawParams, + Callback Reply) { + auto P = LSPBinder::parse(RawParams, Method, "command"); + if (!P) + return Reply(P.takeError()); + (This->*Handler)(*P, std::move(Reply)); + }; +} + +} // namespace clangd +} // namespace clang + +#endif diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -70,6 +70,7 @@ IndexTests.cpp JSONTransportTests.cpp LoggerTests.cpp + LSPBinderTests.cpp LSPClient.cpp ModulesTests.cpp ParsedASTTests.cpp diff --git a/clang-tools-extra/clangd/unittests/LSPBinderTests.cpp b/clang-tools-extra/clangd/unittests/LSPBinderTests.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/LSPBinderTests.cpp @@ -0,0 +1,100 @@ +//===-- LSPBinderTests.cpp ------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "LSPBinder.h" +#include "llvm/Testing/Support/Error.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using testing::HasSubstr; +using testing::UnorderedElementsAre; + +// JSON-serializable type for testing. +struct Foo { + int X; +}; +bool fromJSON(const llvm::json::Value &V, Foo &F, llvm::json::Path P) { + return fromJSON(V, F.X, P.field("X")); +} +llvm::json::Value toJSON(const Foo &F) { return F.X; } + +// Creates a Callback that writes its received value into an Optional. +template +llvm::unique_function)> +capture(llvm::Optional> &Out) { + Out.reset(); + return [&Out](llvm::Expected V) { Out.emplace(std::move(V)); }; +} + +TEST(LSPBinderTest, IncomingCalls) { + LSPBinder::RawHandlers RawHandlers; + LSPBinder Binder{RawHandlers}; + struct Handler { + void plusOne(const Foo &Params, Callback Reply) { + Reply(Foo{Params.X + 1}); + } + void fail(const Foo &Params, Callback Reply) { + Reply(error("X={0}", Params.X)); + } + void notify(const Foo &Params) { + LastNotify = Params.X; + ++NotifyCount; + } + int LastNotify = -1; + int NotifyCount = 0; + }; + + Handler H; + Binder.method("plusOne", &H, &Handler::plusOne); + Binder.method("fail", &H, &Handler::fail); + Binder.notification("notify", &H, &Handler::notify); + Binder.command("cmdPlusOne", &H, &Handler::plusOne); + ASSERT_THAT(RawHandlers.MethodHandlers.keys(), + UnorderedElementsAre("plusOne", "fail")); + ASSERT_THAT(RawHandlers.NotificationHandlers.keys(), + UnorderedElementsAre("notify")); + ASSERT_THAT(RawHandlers.CommandHandlers.keys(), + UnorderedElementsAre("cmdPlusOne")); + llvm::Optional> Reply; + + auto &RawPlusOne = RawHandlers.MethodHandlers["plusOne"]; + RawPlusOne(1, capture(Reply)); + ASSERT_TRUE(Reply.hasValue()); + EXPECT_THAT_EXPECTED(Reply.getValue(), llvm::HasValue(2)); + RawPlusOne("foo", capture(Reply)); + ASSERT_TRUE(Reply.hasValue()); + EXPECT_THAT_EXPECTED( + Reply.getValue(), + llvm::FailedWithMessage( + HasSubstr("failed to decode plusOne request: expected integer"))); + + auto &RawFail = RawHandlers.MethodHandlers["fail"]; + RawFail(2, capture(Reply)); + ASSERT_TRUE(Reply.hasValue()); + EXPECT_THAT_EXPECTED(Reply.getValue(), llvm::FailedWithMessage("X=2")); + + auto &RawNotify = RawHandlers.NotificationHandlers["notify"]; + RawNotify(42); + EXPECT_EQ(H.LastNotify, 42); + EXPECT_EQ(H.NotifyCount, 1); + RawNotify("hi"); // invalid, will be logged + EXPECT_EQ(H.LastNotify, 42); + EXPECT_EQ(H.NotifyCount, 1); + + auto &RawCmdPlusOne = RawHandlers.CommandHandlers["cmdPlusOne"]; + RawCmdPlusOne(1, capture(Reply)); + ASSERT_TRUE(Reply.hasValue()); + EXPECT_THAT_EXPECTED(Reply.getValue(), llvm::HasValue(2)); +} +} // namespace +} // namespace clangd +} // namespace clang