Index: CMakeLists.txt =================================================================== --- CMakeLists.txt +++ CMakeLists.txt @@ -16,7 +16,8 @@ string(REPLACE ${CMAKE_CFG_INTDIR} ${LLVM_BUILD_MODE} CLANG_TOOLS_DIR ${LLVM_RUNTIME_OUTPUT_INTDIR}) llvm_canonicalize_cmake_booleans( - CLANG_ENABLE_STATIC_ANALYZER) + CLANG_ENABLE_STATIC_ANALYZER + CLANGD_BUILD_XPC) configure_lit_site_cfg( ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.in @@ -72,6 +73,10 @@ ) endif() +if(CLANGD_BUILD_XPC) + list(APPEND CLANG_TOOLS_TEST_DEPS clangd-xpc-test-client) +endif() + set(llvm_utils FileCheck count not ) Index: ClangdLSPServer.h =================================================================== --- ClangdLSPServer.h +++ ClangdLSPServer.h @@ -23,29 +23,22 @@ namespace clang { namespace clangd { -class JSONOutput; +class LSPOutput; class SymbolIndex; /// This class provides implementation of an LSP server, glueing the JSON /// dispatch and ClangdServer together. -class ClangdLSPServer : private DiagnosticsConsumer, private ProtocolCallbacks { +class ClangdLSPServer : private DiagnosticsConsumer, public ProtocolCallbacks { public: /// If \p CompileCommandsDir has a value, compile_commands.json will be /// loaded only from \p CompileCommandsDir. Otherwise, clangd will look /// for compile_commands.json in all parent directories of each file. - ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, + ClangdLSPServer(const clangd::CodeCompleteOptions &CCOpts, llvm::Optional CompileCommandsDir, const ClangdServer::Options &Opts); - /// Run LSP server loop, receiving input for it from \p In. \p In must be - /// opened in binary mode. Output will be written using Out variable passed to - /// class constructor. This method must not be executed more than once for - /// each instance of ClangdLSPServer. - /// - /// \return Whether we received a 'shutdown' request before an 'exit' request. - bool run(std::FILE *In, - JSONStreamStyle InputStyle = JSONStreamStyle::Standard); - + bool& getIsDone(); + bool getShutdownRequestReceived() const; private: // Implement DiagnosticsConsumer. void onDiagnosticsReady(PathRef File, std::vector Diagnostics) override; @@ -83,7 +76,6 @@ /// compilation database is changed. void reparseOpenedFiles(); - JSONOutput &Out; /// Used to indicate that the 'shutdown' request was received from the /// Language Server client. bool ShutdownRequestReceived = false; Index: ClangdLSPServer.cpp =================================================================== --- ClangdLSPServer.cpp +++ ClangdLSPServer.cpp @@ -9,6 +9,7 @@ #include "ClangdLSPServer.h" #include "Diagnostics.h" +#include "DispatcherCommon.h" #include "JSONRPCDispatcher.h" #include "SourceCode.h" #include "URI.h" @@ -413,33 +414,13 @@ } } -ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, - const clangd::CodeCompleteOptions &CCOpts, +ClangdLSPServer::ClangdLSPServer(const clangd::CodeCompleteOptions &CCOpts, llvm::Optional CompileCommandsDir, const ClangdServer::Options &Opts) - : Out(Out), NonCachedCDB(std::move(CompileCommandsDir)), CDB(NonCachedCDB), + : NonCachedCDB(std::move(CompileCommandsDir)), CDB(NonCachedCDB), CCOpts(CCOpts), SupportedSymbolKinds(defaultSymbolKinds()), Server(CDB, FSProvider, /*DiagConsumer=*/*this, Opts) {} -bool ClangdLSPServer::run(std::FILE *In, JSONStreamStyle InputStyle) { - assert(!IsDone && "Run was called before"); - - // Set up JSONRPCDispatcher. - JSONRPCDispatcher Dispatcher([](const json::Value &Params) { - replyError(ErrorCode::MethodNotFound, "method not found"); - }); - registerCallbackHandlers(Dispatcher, /*Callbacks=*/*this); - - // Run the Language Server loop. - runLanguageServerLoop(In, Out, InputStyle, Dispatcher, IsDone); - - // Make sure IsDone is set to true after this method exits to ensure assertion - // at the start of the method fires if it's ever executed again. - IsDone = true; - - return ShutdownRequestReceived; -} - std::vector ClangdLSPServer::getFixes(StringRef File, const clangd::Diagnostic &D) { std::lock_guard Lock(FixItsMutex); @@ -482,7 +463,7 @@ } // Publish diagnostics. - Out.writeMessage(json::Object{ + sendMessage(json::Object{ {"jsonrpc", "2.0"}, {"method", "textDocument/publishDiagnostics"}, {"params", @@ -498,3 +479,6 @@ Server.addDocument(FilePath, *DraftMgr.getDraft(FilePath), WantDiagnostics::Auto); } + +bool& ClangdLSPServer::getIsDone() { return IsDone; } +bool ClangdLSPServer::getShutdownRequestReceived() const { return ShutdownRequestReceived; } \ No newline at end of file Index: DispatcherCommon.h =================================================================== --- /dev/null +++ DispatcherCommon.h @@ -0,0 +1,62 @@ +//===--- DispatcherCommon.h - Shared code for dispatchers -----------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_DISPATCHERCOMMON_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_DISPATCHERCOMMON_H + +#include "JSONRPCDispatcher.h" +#include "Trace.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/Chrono.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/SourceMgr.h" +#include + +namespace clang { +namespace clangd { + +extern Key RequestID; +extern Key RequestOut; + +// When tracing, we trace a request and attach the repsonse in reply(). +// Because the Span isn't available, we find the current request using Context. +class RequestSpan { + RequestSpan(llvm::json::Object *Args) : Args(Args) {} + std::mutex Mu; + llvm::json::Object *Args; + static Key> RSKey; + +public: + // Return a context that's aware of the enclosing request, identified by Span. + static Context stash(const trace::Span &Span); + + // If there's an enclosing request and the tracer is interested, calls \p F + // with a llvm::json::Object where request info can be added. + template static void attach(Func &&F) { + auto *RequestArgs = Context::current().get(RSKey); + if (!RequestArgs || !*RequestArgs || !(*RequestArgs)->Args) + return; + std::lock_guard Lock((*RequestArgs)->Mu); + F(*(*RequestArgs)->Args); + } +}; + +void sendMessage(llvm::json::Value &&Message); + +void reply(llvm::json::Value &&Result); + +void replyError(ErrorCode code, const llvm::StringRef &Message); + +void call(StringRef Method, llvm::json::Value &&Params); + +} +} + +#endif \ No newline at end of file Index: DispatcherCommon.cpp =================================================================== --- /dev/null +++ DispatcherCommon.cpp @@ -0,0 +1,89 @@ +//===--- DispatcherCommon.h - Shared code for dispatchers -----------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "DispatcherCommon.h" + +namespace clang { +namespace clangd { + +using namespace llvm; + +Key RequestID; +Key RequestOut; + +Context RequestSpan::stash(const trace::Span &Span) { + return Context::current().derive( + RSKey, std::unique_ptr(new RequestSpan(Span.Args))); +} + +Key> RequestSpan::RSKey; + +void sendMessage(json::Value &&Message) { + Context::current() + .getExisting(RequestOut) + ->writeMessage(Message); +} + +void reply(json::Value &&Result) { + auto ID = Context::current().get(RequestID); + if (!ID) { + elog("Attempted to reply to a notification!"); + return; + } + RequestSpan::attach([&](json::Object &Args) { Args["Reply"] = Result; }); + log("--> reply({0})", *ID); + Context::current() + .getExisting(RequestOut) + ->writeMessage(json::Object{ + {"jsonrpc", "2.0"}, + {"id", *ID}, + {"result", std::move(Result)}, + }); +} + +void replyError(ErrorCode code, const llvm::StringRef &Message) { + elog("Error {0}: {1}", static_cast(code), Message); + RequestSpan::attach([&](json::Object &Args) { + Args["Error"] = json::Object{{"code", static_cast(code)}, + {"message", Message.str()}}; + }); + + if (auto ID = Context::current().get(RequestID)) { + log("--> reply({0}) error: {1}", *ID, Message); + Context::current() + .getExisting(RequestOut) + ->writeMessage(json::Object{ + {"jsonrpc", "2.0"}, + {"id", *ID}, + {"error", json::Object{{"code", static_cast(code)}, + {"message", Message}}}, + }); + } +} + +void call(StringRef Method, json::Value &&Params) { + RequestSpan::attach([&](json::Object &Args) { + Args["Call"] = json::Object{{"method", Method.str()}, {"params", Params}}; + }); + // FIXME: Generate/Increment IDs for every request so that we can get proper + // replies once we need to. + auto ID = 1; + log("--> {0}({1})", Method, ID); + Context::current() + .getExisting(RequestOut) + ->writeMessage(json::Object{ + {"jsonrpc", "2.0"}, + {"id", ID}, + {"method", Method}, + {"params", std::move(Params)}, + }); +} + +} +} Index: Features.inc.in =================================================================== --- /dev/null +++ Features.inc.in @@ -0,0 +1 @@ +#define CLANGD_BUILD_XPC @CLANGD_BUILD_XPC@ Index: JSONRPCDispatcher.h =================================================================== --- JSONRPCDispatcher.h +++ JSONRPCDispatcher.h @@ -1,4 +1,4 @@ -//===--- JSONRPCDispatcher.h - Main JSON parser entry point -----*- C++ -*-===// +//===--- JSONRPCDispatcher.h - Main JSON RPC entry point --------*- C++ -*-===// // // The LLVM Compiler Infrastructure // @@ -11,6 +11,7 @@ #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_JSONRPCDISPATCHER_H #include "Logger.h" +#include "LSPOutput.h" #include "Protocol.h" #include "Trace.h" #include "clang/Basic/LLVM.h" @@ -25,9 +26,7 @@ /// Encapsulates output and logs streams and provides thread-safe access to /// them. -class JSONOutput : public Logger { - // FIXME(ibiryukov): figure out if we can shrink the public interface of - // JSONOutput now that we pass Context everywhere. +class JSONOutput : public LSPOutput { public: JSONOutput(llvm::raw_ostream &Outs, llvm::raw_ostream &Logs, Logger::Level MinLevel, llvm::raw_ostream *InputMirror = nullptr, @@ -36,7 +35,7 @@ InputMirror(InputMirror) {} /// Emit a JSONRPC message. - void writeMessage(const llvm::json::Value &Result); + void writeMessage(const llvm::json::Value &Result) override; /// Write a line to the logging stream. void log(Level, const llvm::formatv_object_base &Message) override; @@ -44,7 +43,7 @@ /// Mirror \p Message into InputMirror stream. Does nothing if InputMirror is /// null. /// Unlike other methods of JSONOutput, mirrorInput is not thread-safe. - void mirrorInput(const Twine &Message); + void mirrorInput(const Twine &Message) override; // Whether output should be pretty-printed. const bool Pretty; @@ -84,7 +83,7 @@ void registerHandler(StringRef Method, Handler H); /// Parses a JSONRPC message and calls the Handler for it. - bool call(const llvm::json::Value &Message, JSONOutput &Out) const; + bool call(const llvm::json::Value &Message, LSPOutput &Out) const; private: llvm::StringMap Handlers; @@ -107,7 +106,7 @@ /// replacements of \r\n with \n. /// We use C-style FILE* for reading as std::istream has unclear interaction /// with signals, which are sent by debuggers on some OSs. -void runLanguageServerLoop(std::FILE *In, JSONOutput &Out, +void runJSONRPCServerLoop(std::FILE *In, JSONOutput &Out, JSONStreamStyle InputStyle, JSONRPCDispatcher &Dispatcher, bool &IsDone); Index: JSONRPCDispatcher.cpp =================================================================== --- JSONRPCDispatcher.cpp +++ JSONRPCDispatcher.cpp @@ -1,4 +1,4 @@ -//===--- JSONRPCDispatcher.cpp - Main JSON parser entry point -------------===// +//===--- JSONRPCDispatcher.h - Main JSON RPC entry point --------*- C++ -*-===// // // The LLVM Compiler Infrastructure // @@ -7,8 +7,8 @@ // //===----------------------------------------------------------------------===// +#include "DispatcherCommon.h" #include "JSONRPCDispatcher.h" -#include "ProtocolHandlers.h" #include "Trace.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" @@ -23,38 +23,6 @@ using namespace clang; using namespace clangd; -namespace { -static Key RequestID; -static Key RequestOut; - -// When tracing, we trace a request and attach the repsonse in reply(). -// Because the Span isn't available, we find the current request using Context. -class RequestSpan { - RequestSpan(llvm::json::Object *Args) : Args(Args) {} - std::mutex Mu; - llvm::json::Object *Args; - static Key> RSKey; - -public: - // Return a context that's aware of the enclosing request, identified by Span. - static Context stash(const trace::Span &Span) { - return Context::current().derive( - RSKey, std::unique_ptr(new RequestSpan(Span.Args))); - } - - // If there's an enclosing request and the tracer is interested, calls \p F - // with a json::Object where request info can be added. - template static void attach(Func &&F) { - auto *RequestArgs = Context::current().get(RSKey); - if (!RequestArgs || !*RequestArgs || !(*RequestArgs)->Args) - return; - std::lock_guard Lock((*RequestArgs)->Mu); - F(*(*RequestArgs)->Args); - } -}; -Key> RequestSpan::RSKey; -} // namespace - void JSONOutput::writeMessage(const json::Value &Message) { std::string S; llvm::raw_string_ostream OS(S); @@ -92,61 +60,6 @@ InputMirror->flush(); } -void clangd::reply(json::Value &&Result) { - auto ID = Context::current().get(RequestID); - if (!ID) { - elog("Attempted to reply to a notification!"); - return; - } - RequestSpan::attach([&](json::Object &Args) { Args["Reply"] = Result; }); - log("--> reply({0})", *ID); - Context::current() - .getExisting(RequestOut) - ->writeMessage(json::Object{ - {"jsonrpc", "2.0"}, - {"id", *ID}, - {"result", std::move(Result)}, - }); -} - -void clangd::replyError(ErrorCode code, const llvm::StringRef &Message) { - elog("Error {0}: {1}", static_cast(code), Message); - RequestSpan::attach([&](json::Object &Args) { - Args["Error"] = json::Object{{"code", static_cast(code)}, - {"message", Message.str()}}; - }); - - if (auto ID = Context::current().get(RequestID)) { - log("--> reply({0}) error: {1}", *ID, Message); - Context::current() - .getExisting(RequestOut) - ->writeMessage(json::Object{ - {"jsonrpc", "2.0"}, - {"id", *ID}, - {"error", json::Object{{"code", static_cast(code)}, - {"message", Message}}}, - }); - } -} - -void clangd::call(StringRef Method, json::Value &&Params) { - RequestSpan::attach([&](json::Object &Args) { - Args["Call"] = json::Object{{"method", Method.str()}, {"params", Params}}; - }); - // FIXME: Generate/Increment IDs for every request so that we can get proper - // replies once we need to. - auto ID = 1; - log("--> {0}({1})", Method, ID); - Context::current() - .getExisting(RequestOut) - ->writeMessage(json::Object{ - {"jsonrpc", "2.0"}, - {"id", ID}, - {"method", Method}, - {"params", std::move(Params)}, - }); -} - void JSONRPCDispatcher::registerHandler(StringRef Method, Handler H) { assert(!Handlers.count(Method) && "Handler already registered!"); Handlers[Method] = std::move(H); @@ -170,7 +83,7 @@ } bool JSONRPCDispatcher::call(const json::Value &Message, - JSONOutput &Out) const { + LSPOutput &Out) const { // Message must be an object with "jsonrpc":"2.0". auto *Object = Message.getAsObject(); if (!Object || Object->getString("jsonrpc") != Optional("2.0")) @@ -236,7 +149,7 @@ // - ferror() or feof() are set. // - Content-Length is missing or empty (protocol error) static llvm::Optional readStandardMessage(std::FILE *In, - JSONOutput &Out) { + LSPOutput &Out) { // A Language Server Protocol message starts with a set of HTTP headers, // delimited by \r\n, and terminated by an empty line (\r\n). unsigned long long ContentLength = 0; @@ -308,7 +221,7 @@ // This is a testing path, so favor simplicity over performance here. // When returning None, feof() or ferror() will be set. static llvm::Optional readDelimitedMessage(std::FILE *In, - JSONOutput &Out) { + LSPOutput &Out) { std::string JSON; std::string Line; while (readLine(In, Line)) { @@ -340,7 +253,7 @@ // sometimes hang rather than exit on other OSes. The interaction between // istreams and signals isn't well-specified, so it's hard to get this right. // The C APIs seem to be clearer in this respect. -void clangd::runLanguageServerLoop(std::FILE *In, JSONOutput &Out, +void clangd::runJSONRPCServerLoop(std::FILE *In, JSONOutput &Out, JSONStreamStyle InputStyle, JSONRPCDispatcher &Dispatcher, bool &IsDone) { Index: LSPOutput.h =================================================================== --- /dev/null +++ LSPOutput.h @@ -0,0 +1,42 @@ +//===--- LSPOutput.h - LSP output interface ---------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_LSPOUTPUT_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_LSPOUTPUT_H + +#include "Logger.h" +#include "clang/Basic/LLVM.h" +#include "llvm/Support/JSON.h" + +namespace clang { +namespace clangd { + +/// Encapsulates output and logs streams and provides thread-safe access to +/// them. +class LSPOutput : public Logger { + // FIXME(ibiryukov): figure out if we can shrink the public interface of + // LSPOutput now that we pass Context everywhere. +public: + /// Emit LSP message. + virtual void writeMessage(const llvm::json::Value &Result) = 0; + + /// Write a line to log. + virtual void log(Level, const llvm::formatv_object_base &Message) = 0; + + /// FIXME: Does it make sense for all implementations? + /// Mirror \p Message into InputMirror stream. Does nothing if InputMirror is + /// null. + /// Unlike other methods of LSPOutput, mirrorInput is not thread-safe. + virtual void mirrorInput(const Twine &Message) = 0; +}; + +} // namespace clangd +} // namespace clang + +#endif Index: ProtocolHandlers.h =================================================================== --- ProtocolHandlers.h +++ ProtocolHandlers.h @@ -7,11 +7,11 @@ // //===----------------------------------------------------------------------===// // -// ProtocolHandlers translates incoming JSON requests from JSONRPCDispatcher -// into method calls on ClangLSPServer. +// ProtocolHandlers translates incoming LSP requests from JSONRPCDispatcher +// or XPCDispatcher into method calls on ClangLSPServer. // // Currently it parses requests into objects, but the ClangLSPServer is -// responsible for producing JSON responses. We should move that here, too. +// responsible for producing LSP responses. We should move that here, too. // //===----------------------------------------------------------------------===// @@ -23,6 +23,8 @@ #include "llvm/ADT/Twine.h" #include "llvm/Support/raw_ostream.h" +using namespace llvm; + namespace clang { namespace clangd { @@ -57,8 +59,64 @@ virtual void onChangeConfiguration(DidChangeConfigurationParams &Params) = 0; }; -void registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, - ProtocolCallbacks &Callbacks); +// Helper for attaching ProtocolCallbacks methods to a JSONRPCDispatcher. +// Invoke like: Registerer("foo", &ProtocolCallbacks::onFoo) +// onFoo should be: void onFoo(Ctx &C, FooParams &Params) +// FooParams should have a fromJSON function. +template +struct HandlerRegisterer { + template + void operator()(StringRef Method, void (ProtocolCallbacks::*Handler)(Param)) { + // Capture pointers by value, as the lambda will outlive this object. + auto *Callbacks = this->Callbacks; + Dispatcher.registerHandler(Method, [=](const json::Value &RawParams) { + typename std::remove_reference::type P; + if (fromJSON(RawParams, P)) { + (Callbacks->*Handler)(P); + } else { + elog("Failed to decode {0} request.", Method); + } + }); + } + + DispatcherType &Dispatcher; + ProtocolCallbacks *Callbacks; +}; + +template +void registerCallbackHandlers(DispatcherType &Dispatcher, + ProtocolCallbacks &Callbacks) { + HandlerRegisterer Register{Dispatcher, &Callbacks}; + + Register("initialize", &ProtocolCallbacks::onInitialize); + Register("shutdown", &ProtocolCallbacks::onShutdown); + Register("exit", &ProtocolCallbacks::onExit); + Register("textDocument/didOpen", &ProtocolCallbacks::onDocumentDidOpen); + Register("textDocument/didClose", &ProtocolCallbacks::onDocumentDidClose); + Register("textDocument/didChange", &ProtocolCallbacks::onDocumentDidChange); + Register("textDocument/rangeFormatting", + &ProtocolCallbacks::onDocumentRangeFormatting); + Register("textDocument/onTypeFormatting", + &ProtocolCallbacks::onDocumentOnTypeFormatting); + Register("textDocument/formatting", &ProtocolCallbacks::onDocumentFormatting); + Register("textDocument/codeAction", &ProtocolCallbacks::onCodeAction); + Register("textDocument/completion", &ProtocolCallbacks::onCompletion); + Register("textDocument/signatureHelp", &ProtocolCallbacks::onSignatureHelp); + Register("textDocument/definition", &ProtocolCallbacks::onGoToDefinition); + Register("textDocument/switchSourceHeader", + &ProtocolCallbacks::onSwitchSourceHeader); + Register("textDocument/rename", &ProtocolCallbacks::onRename); + Register("textDocument/hover", &ProtocolCallbacks::onHover); + Register("textDocument/documentSymbol", &ProtocolCallbacks::onDocumentSymbol); + Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent); + Register("workspace/executeCommand", &ProtocolCallbacks::onCommand); + Register("textDocument/documentHighlight", + &ProtocolCallbacks::onDocumentHighlight); + Register("workspace/didChangeConfiguration", + &ProtocolCallbacks::onChangeConfiguration); + Register("workspace/symbol", &ProtocolCallbacks::onWorkspaceSymbol); +} + } // namespace clangd } // namespace clang Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ /dev/null @@ -1,78 +0,0 @@ -//===--- ProtocolHandlers.cpp - LSP callbacks -----------------------------===// -// -// The LLVM Compiler Infrastructure -// -// This file is distributed under the University of Illinois Open Source -// License. See LICENSE.TXT for details. -// -//===----------------------------------------------------------------------===// - -#include "ProtocolHandlers.h" -#include "ClangdLSPServer.h" -#include "ClangdServer.h" -#include "DraftStore.h" -#include "Trace.h" - -using namespace clang; -using namespace clang::clangd; -using namespace llvm; - -namespace { - -// Helper for attaching ProtocolCallbacks methods to a JSONRPCDispatcher. -// Invoke like: Registerer("foo", &ProtocolCallbacks::onFoo) -// onFoo should be: void onFoo(Ctx &C, FooParams &Params) -// FooParams should have a fromJSON function. -struct HandlerRegisterer { - template - void operator()(StringRef Method, void (ProtocolCallbacks::*Handler)(Param)) { - // Capture pointers by value, as the lambda will outlive this object. - auto *Callbacks = this->Callbacks; - Dispatcher.registerHandler(Method, [=](const json::Value &RawParams) { - typename std::remove_reference::type P; - if (fromJSON(RawParams, P)) { - (Callbacks->*Handler)(P); - } else { - elog("Failed to decode {0} request.", Method); - } - }); - } - - JSONRPCDispatcher &Dispatcher; - ProtocolCallbacks *Callbacks; -}; - -} // namespace - -void clangd::registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, - ProtocolCallbacks &Callbacks) { - HandlerRegisterer Register{Dispatcher, &Callbacks}; - - Register("initialize", &ProtocolCallbacks::onInitialize); - Register("shutdown", &ProtocolCallbacks::onShutdown); - Register("exit", &ProtocolCallbacks::onExit); - Register("textDocument/didOpen", &ProtocolCallbacks::onDocumentDidOpen); - Register("textDocument/didClose", &ProtocolCallbacks::onDocumentDidClose); - Register("textDocument/didChange", &ProtocolCallbacks::onDocumentDidChange); - Register("textDocument/rangeFormatting", - &ProtocolCallbacks::onDocumentRangeFormatting); - Register("textDocument/onTypeFormatting", - &ProtocolCallbacks::onDocumentOnTypeFormatting); - Register("textDocument/formatting", &ProtocolCallbacks::onDocumentFormatting); - Register("textDocument/codeAction", &ProtocolCallbacks::onCodeAction); - Register("textDocument/completion", &ProtocolCallbacks::onCompletion); - Register("textDocument/signatureHelp", &ProtocolCallbacks::onSignatureHelp); - Register("textDocument/definition", &ProtocolCallbacks::onGoToDefinition); - Register("textDocument/switchSourceHeader", - &ProtocolCallbacks::onSwitchSourceHeader); - Register("textDocument/rename", &ProtocolCallbacks::onRename); - Register("textDocument/hover", &ProtocolCallbacks::onHover); - Register("textDocument/documentSymbol", &ProtocolCallbacks::onDocumentSymbol); - Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent); - Register("workspace/executeCommand", &ProtocolCallbacks::onCommand); - Register("textDocument/documentHighlight", - &ProtocolCallbacks::onDocumentHighlight); - Register("workspace/didChangeConfiguration", - &ProtocolCallbacks::onChangeConfiguration); - Register("workspace/symbol", &ProtocolCallbacks::onWorkspaceSymbol); -} Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -52,3 +52,7 @@ LLVMSupport LLVMTestingSupport ) + +if (CLANGD_BUILD_XPC) + add_subdirectory(xpc) +endif () Index: clangd/xpc/CMakeLists.txt =================================================================== --- /dev/null +++ clangd/xpc/CMakeLists.txt @@ -0,0 +1,22 @@ +set(LLVM_LINK_COMPONENTS + support + ) + +get_filename_component(CLANGD_SOURCE_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/../../clangd REALPATH) +include_directories( + ${CLANGD_SOURCE_DIR} + ) + +add_extra_unittest(ClangdXpcTests + ConversionTests.cpp + ) + +# FIXME: Factor out json::Expr from clangDaemon +target_link_libraries(ClangdXpcTests + PRIVATE + clangdXpcJsonConversions + clangDaemon + LLVMSupport + LLVMTestingSupport + ) Index: clangd/xpc/ConversionTests.cpp =================================================================== --- /dev/null +++ clangd/xpc/ConversionTests.cpp @@ -0,0 +1,454 @@ +//===-- ConversionTests.cpp --------------------------*- C++ -*-----------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "xpc/Conversion.h" +#include "gtest/gtest.h" + +#include + +namespace clang { +namespace clangd { +namespace { + +using namespace llvm; + +TEST(JsonXpcConversionTest, Null) { + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(nullptr)), + xpc_null_create() + ) + ); + EXPECT_TRUE( + json::Value(nullptr) == xpcToJson(xpc_null_create()) + ); +} + +TEST(JsonXpcConversionTest, Bool) { + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(true)), + xpc_bool_create(true) + ) + ); + EXPECT_TRUE( + json::Value(false) == xpcToJson(xpc_bool_create(false)) + ); +} + +TEST(JsonXpcConversionTest, Number) { + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(3.14)), + xpc_double_create(3.14) + ) + ); + EXPECT_TRUE( + json::Value(3.14) == xpcToJson(xpc_double_create(3.14)) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(42)), + xpc_double_create(42) + ) + ); + EXPECT_TRUE( + json::Value(42) == xpcToJson(xpc_double_create(42)) + ); + EXPECT_TRUE( + json::Value(42) == xpcToJson(xpc_int64_create(42)) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(-100)), + xpc_double_create(-100) + ) + ); + EXPECT_TRUE( + json::Value(-100) == xpcToJson(xpc_double_create(-100)) + ); + + int64_t bigPositiveValue = std::numeric_limits::max(); + ++bigPositiveValue; + int64_t bigNegativeValue = std::numeric_limits::min(); + --bigNegativeValue; + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(bigPositiveValue)), + xpc_double_create(bigPositiveValue) + ) + ); + EXPECT_TRUE( + json::Value(bigPositiveValue) == xpcToJson(xpc_double_create(bigPositiveValue)) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(bigNegativeValue)), + xpc_double_create(bigNegativeValue) + ) + ); + EXPECT_TRUE( + json::Value(bigNegativeValue) == xpcToJson(xpc_double_create(bigNegativeValue)) + ); +} + +TEST(JsonXpcConversionTest, String) { + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value("foo")), + xpc_string_create("foo") + ) + ); + EXPECT_TRUE( + json::Value("foo") == xpcToJson(xpc_string_create("foo")) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value("")), + xpc_string_create("") + ) + ); + EXPECT_TRUE( + json::Value("") == xpcToJson(xpc_string_create("")) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value("123")), + xpc_string_create("123") + ) + ); + EXPECT_TRUE( + json::Value("123") == xpcToJson(xpc_string_create("123")) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(" ")), + xpc_string_create(" ") + ) + ); + EXPECT_TRUE( + json::Value(" ") == xpcToJson(xpc_string_create(" ")) + ); + + // Testing two different "patterns" just in case. + std::string kBStringOfAs("A", 1024); + std::string kBStringOfBs("B", 1024); + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(kBStringOfAs.c_str())), + xpc_string_create(kBStringOfAs.c_str()) + ) + ); + EXPECT_TRUE( + json::Value(kBStringOfAs.c_str()) == xpcToJson(xpc_string_create(kBStringOfAs.c_str())) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(json::Value(kBStringOfBs.c_str())), + xpc_string_create(kBStringOfBs.c_str()) + ) + ); + EXPECT_TRUE( + json::Value(kBStringOfBs.c_str()) == xpcToJson(xpc_string_create(kBStringOfBs.c_str())) + ); +} + +TEST(JsonXpcConversionTest, Array) { + json::Value JsonArray{true, "foo", nullptr, 42}; + + xpc_object_t XpcArray = []() { + std::vector XpcArrayElements; + XpcArrayElements.emplace_back(xpc_bool_create(true)); + XpcArrayElements.emplace_back(xpc_string_create("foo")); + XpcArrayElements.emplace_back(xpc_null_create()); + XpcArrayElements.emplace_back(xpc_double_create(42)); + + return xpc_array_create(XpcArrayElements.data(), XpcArrayElements.size()); + }(); + + // Per-element checks just speed up eventual debugging - testing only one direction. + EXPECT_TRUE( + xpc_equal( + jsonToXpc( (*JsonArray.getAsArray())[0] ), + xpc_array_get_value(XpcArray, 0) + ) + ); + EXPECT_TRUE( + xpc_equal( + jsonToXpc( (*JsonArray.getAsArray())[1] ), + xpc_array_get_value(XpcArray, 1) + ) + ); + EXPECT_TRUE( + xpc_equal( + jsonToXpc( (*JsonArray.getAsArray())[2] ), + xpc_array_get_value(XpcArray, 2) + ) + ); + EXPECT_TRUE( + xpc_equal( + jsonToXpc( (*JsonArray.getAsArray())[3] ), + xpc_array_get_value(XpcArray, 3) + ) + ); + + json::Value NestedJsonArray{ "foo", JsonArray, 123456789 }; + + xpc_object_t NestedXpcArray = [&](){ + std::vector NestedXpcArrayElements; + NestedXpcArrayElements.emplace_back(xpc_string_create("foo")); + NestedXpcArrayElements.emplace_back(XpcArray); + NestedXpcArrayElements.emplace_back(xpc_double_create(123456789)); + + return xpc_array_create(NestedXpcArrayElements.data(), NestedXpcArrayElements.size()); + }(); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(NestedJsonArray), + NestedXpcArray + ) + ); + EXPECT_TRUE( + NestedJsonArray == xpcToJson(NestedXpcArray) + ); +} + +TEST(JsonXpcConversionTest, Dictionary) { + json::Value JsonDict( + json::Object{ + {"a", true}, + {"b", "foo"}, + {"c", nullptr}, + {"d", 42} + } + ); + + xpc_object_t XpcDict = []() { + std::vector XpcDictKeys; + std::vector XpcDictValues; + XpcDictKeys.emplace_back("a"); + XpcDictValues.emplace_back(xpc_bool_create(true)); + XpcDictKeys.emplace_back("b"); + XpcDictValues.emplace_back(xpc_string_create("foo")); + XpcDictKeys.emplace_back("c"); + XpcDictValues.emplace_back(xpc_null_create()); + XpcDictKeys.emplace_back("d"); + XpcDictValues.emplace_back(xpc_double_create(42)); + + std::vector XpcDictKeysCStr = { + XpcDictKeys.at(0).c_str(), + XpcDictKeys.at(1).c_str(), + XpcDictKeys.at(2).c_str(), + XpcDictKeys.at(3).c_str() + }; + + return xpc_dictionary_create(XpcDictKeysCStr.data(), XpcDictValues.data(), XpcDictValues.size()); + }(); + + // Per-element checks just speed up eventual debugging - testing only one direction. + EXPECT_TRUE( + xpc_equal( + jsonToXpc(*JsonDict.getAsObject()->get("a")), + xpc_dictionary_get_value(XpcDict, "a") + ) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(*JsonDict.getAsObject()->get("b")), + xpc_dictionary_get_value(XpcDict, "b") + ) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(*JsonDict.getAsObject()->get("c")), + xpc_dictionary_get_value(XpcDict, "c") + ) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(*JsonDict.getAsObject()->get("d")), + xpc_dictionary_get_value(XpcDict, "d") + ) + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(JsonDict), + XpcDict + ) + ); + + xpc_object_t NestedXpcDict = [&](){ + std::vector NestedXpcDictKeys; + std::vector NestedXpcDictValues; + { + NestedXpcDictKeys.emplace_back("3"); + NestedXpcDictValues.emplace_back(xpc_double_create(42.24)); + + NestedXpcDictKeys.emplace_back("3.1"); + NestedXpcDictValues.emplace_back(XpcDict); + + NestedXpcDictKeys.emplace_back("3.14"); + NestedXpcDictValues.emplace_back(xpc_string_create("foo")); + } + std::vector NestedXpcDictKeysCStr = { + NestedXpcDictKeys.at(0).c_str(), + NestedXpcDictKeys.at(1).c_str(), + NestedXpcDictKeys.at(2).c_str() + }; + return xpc_dictionary_create(NestedXpcDictKeysCStr.data(), NestedXpcDictValues.data(), NestedXpcDictValues.size()); + }(); + + json::Value NestedJsonDict( + json::Object{ + {"3", 42.24}, + {"3.1", JsonDict}, + {"3.14", "foo"} + } + ); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(NestedJsonDict), + NestedXpcDict + ) + ); + EXPECT_TRUE( + NestedJsonDict == xpcToJson(NestedXpcDict) + ); +} + +TEST(JsonXpcConversionTest, ArrayContainingDictionary) { + json::Value JsonDict( + json::Object{ + {"a", true}, + {"b", "foo"}, + {"c", nullptr}, + {"d", 42} + } + ); + + json::Value JsonArray{true, "foo", nullptr, JsonDict, 42}; + + xpc_object_t XpcArray = [](){ + xpc_object_t XpcDict = [](){ + std::vector XpcDictKeys; + std::vector XpcDictValues; + XpcDictKeys.emplace_back("a"); + XpcDictValues.emplace_back(xpc_bool_create(true)); + XpcDictKeys.emplace_back("b"); + XpcDictValues.emplace_back(xpc_string_create("foo")); + XpcDictKeys.emplace_back("c"); + XpcDictValues.emplace_back(xpc_null_create()); + XpcDictKeys.emplace_back("d"); + XpcDictValues.emplace_back(xpc_double_create(42)); + + std::vector XpcDictKeysCStr = { + XpcDictKeys.at(0).c_str(), + XpcDictKeys.at(1).c_str(), + XpcDictKeys.at(2).c_str(), + XpcDictKeys.at(3).c_str() + }; + + return xpc_dictionary_create(XpcDictKeysCStr.data(), XpcDictValues.data(), XpcDictValues.size()); + }(); + + std::vector XpcArrayElements; + XpcArrayElements.emplace_back(xpc_bool_create(true)); + XpcArrayElements.emplace_back(xpc_string_create("foo")); + XpcArrayElements.emplace_back(xpc_null_create()); + XpcArrayElements.emplace_back(XpcDict); + XpcArrayElements.emplace_back(xpc_double_create(42)); + + return xpc_array_create(XpcArrayElements.data(), XpcArrayElements.size()); + }(); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(JsonArray), + XpcArray + ) + ); + EXPECT_TRUE( + JsonArray == xpcToJson(XpcArray) + ); +} + +TEST(JsonXpcConversionTest, DictionaryContainingArray) { + json::Value JsonDict( + json::Object{ + {"a", true}, + {"b", "foo"}, + {"c", json::Value{true, "foo", nullptr, 42}}, + {"d", nullptr}, + {"e", 42} + } + ); + + xpc_object_t XpcDict = [](){ + xpc_object_t XpcArray = [](){ + std::vector XpcArrayElements; + XpcArrayElements.emplace_back(xpc_bool_create(true)); + XpcArrayElements.emplace_back(xpc_string_create("foo")); + XpcArrayElements.emplace_back(xpc_null_create()); + XpcArrayElements.emplace_back(xpc_double_create(42)); + + return xpc_array_create(XpcArrayElements.data(), XpcArrayElements.size()); + }(); + + std::vector XpcDictKeys; + std::vector XpcDictValues; + XpcDictKeys.emplace_back("a"); + XpcDictValues.emplace_back(xpc_bool_create(true)); + XpcDictKeys.emplace_back("b"); + XpcDictValues.emplace_back(xpc_string_create("foo")); + XpcDictKeys.emplace_back("c"); + XpcDictValues.emplace_back(XpcArray); + XpcDictKeys.emplace_back("d"); + XpcDictValues.emplace_back(xpc_null_create()); + XpcDictKeys.emplace_back("e"); + XpcDictValues.emplace_back(xpc_double_create(42)); + + std::vector XpcDictKeysCStr = { + XpcDictKeys.at(0).c_str(), + XpcDictKeys.at(1).c_str(), + XpcDictKeys.at(2).c_str(), + XpcDictKeys.at(3).c_str(), + XpcDictKeys.at(4).c_str() + }; + + return xpc_dictionary_create(XpcDictKeysCStr.data(), XpcDictValues.data(), XpcDictValues.size()); + }(); + + EXPECT_TRUE( + xpc_equal( + jsonToXpc(JsonDict), + XpcDict + ) + ); + EXPECT_TRUE( + JsonDict == xpcToJson(XpcDict) + ); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clangd/xpc/initialize.test =================================================================== --- /dev/null +++ clangd/xpc/initialize.test @@ -0,0 +1,10 @@ +# RUN: clangd-xpc-test-client < %s | FileCheck %s +# REQUIRES: clangd-xpc-support + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"test:///workspace","capabilities":{},"trace":"off"}} +# CHECK: {"id":0,"jsonrpc":"2.0","result":{"capabilities":{"codeActionProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",">",":"]},"definitionProvider":true,"documentFormattingProvider":true,"documentHighlightProvider":true,"documentOnTypeFormattingProvider":{"firstTriggerCharacter":"}","moreTriggerCharacter":[]},"documentRangeFormattingProvider":true,"documentSymbolProvider":true,"executeCommandProvider":{"commands":["clangd.applyFix"]},"hoverProvider":true,"renameProvider":true,"signatureHelpProvider":{"triggerCharacters":["(",","]},"textDocumentSync":2,"workspaceSymbolProvider":true}}} + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +# CHECK: {"id":3,"jsonrpc":"2.0","result":null} + +{"jsonrpc":"2.0","method":"exit"} Index: lit.cfg =================================================================== --- lit.cfg +++ lit.cfg @@ -117,6 +117,10 @@ if platform.system() not in ['Windows']: config.available_features.add('ansi-escape-sequences') +# XPC support for Clangd. +if config.clangd_xpc_support: + config.available_features.add('clangd-xpc-support') + if config.clang_staticanalyzer: config.available_features.add('static-analyzer') check_clang_tidy = os.path.join( Index: lit.site.cfg.in =================================================================== --- lit.site.cfg.in +++ lit.site.cfg.in @@ -11,6 +11,7 @@ config.python_executable = "@PYTHON_EXECUTABLE@" config.target_triple = "@TARGET_TRIPLE@" config.clang_staticanalyzer = @CLANG_ENABLE_STATIC_ANALYZER@ +config.clangd_xpc_support = @CLANGD_BUILD_XPC@ # Support substitution of the tools and libs dirs with user parameters. This is # used when we can't determine the tool dir at configuration time. Index: tool/CMakeLists.txt =================================================================== --- tool/CMakeLists.txt +++ tool/CMakeLists.txt @@ -1,4 +1,5 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/..) add_clang_tool(clangd ClangdMain.cpp @@ -8,6 +9,11 @@ support ) +set(CLANGD_XPC_LIBS "") +if(CLANGD_BUILD_XPC) + list(APPEND CLANGD_XPC_LIBS "clangdXpcJsonConversions" "clangdXpcDispatcher") +endif() + target_link_libraries(clangd PRIVATE clangBasic @@ -17,4 +23,5 @@ clangSema clangTooling clangToolingCore + ${CLANGD_XPC_LIBS} ) Index: tool/ClangdMain.cpp =================================================================== --- tool/ClangdMain.cpp +++ tool/ClangdMain.cpp @@ -8,6 +8,7 @@ //===----------------------------------------------------------------------===// #include "ClangdLSPServer.h" +#include "Features.inc" #include "JSONRPCDispatcher.h" #include "Path.h" #include "Trace.h" @@ -20,11 +21,15 @@ #include "llvm/Support/raw_ostream.h" #include "clang/Basic/Version.h" #include -#include #include #include #include +#ifdef CLANGD_BUILD_XPC +#include "xpc/XPCDispatcher.h" +#include +#endif + using namespace clang; using namespace clang::clangd; @@ -231,12 +236,6 @@ if (Tracer) TracingSession.emplace(*Tracer); - JSONOutput Out(llvm::outs(), llvm::errs(), LogLevel, - InputMirrorStream ? InputMirrorStream.getPointer() : nullptr, - PrettyPrint); - - clangd::LoggingSession LoggingSession(Out); - // If --compile-commands-dir arg was invoked, check value and override default // path. llvm::Optional CompileCommandsDirPath; @@ -277,11 +276,44 @@ CCOpts.BundleOverloads = CompletionStyle != Detailed; CCOpts.ShowOrigins = ShowOrigins; - // Initialize and run ClangdLSPServer. - ClangdLSPServer LSPServer(Out, CCOpts, CompileCommandsDirPath, Opts); - constexpr int NoShutdownRequestErrorCode = 1; + ClangdLSPServer LSPServer(CCOpts, CompileCommandsDirPath, Opts); + llvm::set_thread_name("clangd.main"); // Change stdin to binary to not lose \r\n on windows. llvm::sys::ChangeStdinToBinary(); - return LSPServer.run(stdin, InputStyle) ? 0 : NoShutdownRequestErrorCode; + +#ifdef CLANGD_BUILD_XPC + if (getenv("CLANGD_AS_XPC_SERVICE")) { + XPCDispatcher Dispatcher([](const json::Value &Params) { + replyError(ErrorCode::MethodNotFound, "method not found"); + }); + registerCallbackHandlers(Dispatcher, /*Callbacks=*/LSPServer); + + XPCLSPOutput Out( + llvm::errs(), LogLevel, + InputMirrorStream.hasValue() ? InputMirrorStream.getPointer(): nullptr); + + clangd::LoggingSession LoggingSession(Out); + + runXPCServerLoop(Dispatcher, Out, LSPServer.getIsDone()); + } else +#endif + { + JSONRPCDispatcher Dispatcher([](const json::Value &Params) { + replyError(ErrorCode::MethodNotFound, "method not found"); + }); + registerCallbackHandlers(Dispatcher, /*Callbacks=*/LSPServer); + + + JSONOutput Out(llvm::outs(), llvm::errs(), LogLevel, + InputMirrorStream ? InputMirrorStream.getPointer() : nullptr, + PrettyPrint); + + clangd::LoggingSession LoggingSession(Out); + + runJSONRPCServerLoop(stdin, Out, InputStyle, Dispatcher, LSPServer.getIsDone()); + } + + constexpr int NoShutdownRequestErrorCode = 1; + return LSPServer.getShutdownRequestReceived() ? 0 : NoShutdownRequestErrorCode; } Index: xpc/CMakeLists.txt =================================================================== --- /dev/null +++ xpc/CMakeLists.txt @@ -0,0 +1,29 @@ +set(CLANGD_XPC_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(CLANGD_XPC_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}") + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") +include(CreateClangdXPCFramework) + +add_subdirectory(framework) +add_subdirectory(test-client) + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/../ +) + +set(LLVM_LINK_COMPONENTS + Support + ) + +# Needed by LLVM's CMake checks because this file defines multiple targets. +set(LLVM_OPTIONAL_SOURCES Conversion.cpp XPCDispatcher.cpp) + +add_clang_library(clangdXpcJsonConversions + Conversion.cpp + ) + +add_clang_library(clangdXpcDispatcher + XPCDispatcher.cpp + DEPENDS clangdXpcJsonConversions + LINK_LIBS clangdXpcJsonConversions + ) Index: xpc/Conversion.h =================================================================== --- /dev/null +++ xpc/Conversion.h @@ -0,0 +1,25 @@ +//===--- Conversion.h - LSP data (de-)serialization through XPC -*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_XPCJSONCONVERSIONS_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_XPCJSONCONVERSIONS_H + +#include "llvm/Support/JSON.h" +#include + +namespace clang { +namespace clangd { + +xpc_object_t jsonToXpc(const llvm::json::Value& json); +llvm::json::Value xpcToJson(const xpc_object_t& json); + +} // namespace clangd +} // namespace clang + +#endif Index: xpc/Conversion.cpp =================================================================== --- /dev/null +++ xpc/Conversion.cpp @@ -0,0 +1,100 @@ +//===--- Conversion.cpp - LSP data (de-)serialization through XPC - C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "xpc/Conversion.h" + +#include "Logger.h" + +#include +#include + +using namespace clang; +using namespace clangd; + +using namespace llvm; + +xpc_object_t clangd::jsonToXpc(const json::Value& json) { + switch(json.kind()) { + case json::Value::Null: return xpc_null_create(); + case json::Value::Boolean: return xpc_bool_create(json.getAsBoolean().getValue()); + case json::Value::Number: return xpc_double_create(json.getAsNumber().getValue()); + case json::Value::String: return xpc_string_create(json.getAsString().getValue().str().c_str()); + case json::Value::Array: { + std::vector elements; + for(const auto& e : *json.getAsArray()) + elements.emplace_back( jsonToXpc(e) ); + + return xpc_array_create(elements.data(), elements.size()); + } + case json::Value::Object: { + const auto* const JsonObject = json.getAsObject(); + + std::vector keys; + std::vector keysCStr; + std::vector values; + + keys.reserve(JsonObject->size()); + keysCStr.reserve(JsonObject->size()); + values.reserve(JsonObject->size()); + + for(const auto& k_v : *JsonObject) { + keys.emplace_back(static_cast(k_v.first).str()); + values.emplace_back(jsonToXpc(k_v.second)); + } + // Get pointers only now so there's no possible re-allocation of keys. + for(const auto& k : keys) + keysCStr.emplace_back(k.c_str()); + + assert(keysCStr.size() == values.size() && "keys - data size mismatch"); + return xpc_dictionary_create(keysCStr.data(), values.data(), keysCStr.size()); + } + } + llvm_unreachable("unsupported JSON data type in jsonToXpc() conversion"); +} + +json::Value clangd::xpcToJson(const xpc_object_t& xpcObj) { + const xpc_type_t objType = xpc_get_type(xpcObj); + if (objType == XPC_TYPE_NULL) + return json::Value(nullptr); + if (objType == XPC_TYPE_BOOL) + return json::Value(xpc_bool_get_value(xpcObj)); + if (objType == XPC_TYPE_DOUBLE) + return json::Value(xpc_double_get_value(xpcObj)); + if (objType == XPC_TYPE_INT64) + return json::Value(xpc_int64_get_value(xpcObj)); + if (objType == XPC_TYPE_UINT64) + return json::Value( static_cast(xpc_uint64_get_value(xpcObj)) ); + if (objType == XPC_TYPE_STRING) + return json::Value(xpc_string_get_string_ptr(xpcObj)); + if (objType == XPC_TYPE_ARRAY) { + __block json::Array result; + xpc_array_apply( + xpcObj, + ^bool(size_t /* index */, xpc_object_t value) { + result.emplace_back(xpcToJson(value)); + return true; + } + ); + return std::move(result); + } + if (objType == XPC_TYPE_DICTIONARY) { + __block json::Object result; + xpc_dictionary_apply( + xpcObj, + ^bool(const char *key, xpc_object_t value) { + result.try_emplace(json::ObjectKey(key), xpcToJson(value)); + return true; + } + ); + return std::move(result); + } + elog("unsupported JSON data type in xpcToJson() conversion"); + llvm_unreachable("unsupported JSON data type in xpcToJson() conversion"); + return json::Value(nullptr); +} \ No newline at end of file Index: xpc/README.txt =================================================================== --- /dev/null +++ xpc/README.txt @@ -0,0 +1,6 @@ +This directory contains: +- the XPC transport layer (alternative transport layer to JSON-RPC) +- XPC framework wrapper that wraps around Clangd to make it a valid XPC service +- XPC test-client + +MacOS only. Feature is guarded by CLANGD_BUILD_XPC, including whole xpc/ dir. \ No newline at end of file Index: xpc/XPCDispatcher.h =================================================================== --- /dev/null +++ xpc/XPCDispatcher.h @@ -0,0 +1,95 @@ +//===--- XPCDispatcher.h - Main XPC service entry point ---------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_XPCDISPATCHER_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_XPC_XPCDISPATCHER_H + +#include "Logger.h" +#include "LSPOutput.h" +#include "Protocol.h" +#include "Trace.h" +#include "clang/Basic/LLVM.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/JSON.h" +#include +#include + +#include + +namespace clang { +namespace clangd { + +using namespace llvm; + +class XPCLSPOutput : public LSPOutput { +public: + XPCLSPOutput(llvm::raw_ostream &Logs, Logger::Level MinLevel, + llvm::raw_ostream *InputMirror = nullptr) + : clientConnection(nullptr), MinLevel(MinLevel), Logs(Logs), + InputMirror{InputMirror} { } + + /// Set's clientConnection for output. + /// Must be either reset every time XPC event handler is called or connection + /// has to be retained through xpc_retain(). + void resetClientConnection(xpc_connection_t newClientConnection); + + /// Emit an LSP message. + void writeMessage(const json::Value &Result) override; + + /// Write a line to the logging stream. + void log(Level, const llvm::formatv_object_base &Message) override; + + /// Mirror \p Message into InputMirror stream. Does nothing if InputMirror is + /// null. + /// Unlike other methods, mirrorInput is not thread-safe. + void mirrorInput(const Twine &) override; + +private: + xpc_connection_t clientConnection; + Logger::Level MinLevel; + llvm::raw_ostream &Logs; + llvm::raw_ostream *InputMirror; + + std::mutex LogStreamMutex; +}; + +/// Parses the LSP "header" and calls the right registered Handler. +class XPCDispatcher { +public: + // A handler responds to requests for a particular method name. + using Handler = std::function; + + /// UnknownHandler is called when an unknown method is received. + XPCDispatcher(Handler UnknownHandler) + : UnknownHandler(std::move(UnknownHandler)) {} + + /// Registers a Handler for the specified Method. + void registerHandler(StringRef Method, Handler H); + + /// Parses a JSONRPC message and calls the Handler for it. + bool call(const json::Value &Message, LSPOutput &Out) const; + +private: + llvm::StringMap Handlers; + Handler UnknownHandler; +}; + +/// Parses input queries from LSP client (coming through XPC connections) and +/// runs call method of \p Dispatcher for each query. +/// If \p InputJSONMirror is set every received request is logged in JSON form. +/// After handling each query checks if \p IsDone is set true and exits +/// the loop if it is. +void runXPCServerLoop(XPCDispatcher &Dispatcher, XPCLSPOutput &Out, + bool &IsDone); + +} // namespace clangd +} // namespace clang + +#endif Index: xpc/XPCDispatcher.cpp =================================================================== --- /dev/null +++ xpc/XPCDispatcher.cpp @@ -0,0 +1,164 @@ +//===--- XPCDispatcher.h - Main XPC service entry point ---------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "XPCDispatcher.h" + +#include "DispatcherCommon.h" +#include "xpc/Conversion.h" +#include "Trace.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/Chrono.h" +#include "llvm/Support/SourceMgr.h" +#include + +#include + +using namespace clang; +using namespace clangd; + +void XPCDispatcher::registerHandler(StringRef Method, Handler H) { + assert(!Handlers.count(Method) && "Handler already registered!"); + Handlers[Method] = std::move(H); +} + +bool XPCDispatcher::call(const json::Value &Message, LSPOutput &Out) const { + // Message must be an object with "jsonrpc":"2.0". + auto *Object = Message.getAsObject(); + if (!Object || Object->getString("jsonrpc") != Optional("2.0")) + return false; + // ID may be any JSON value. If absent, this is a notification. + llvm::Optional ID; + if (auto *I = Object->get("id")) + ID = std::move(*I); + // Method must be given. + auto Method = Object->getString("method"); + if (!Method) + return false; + // Params should be given, use null if not. + json::Value Params = nullptr; + if (auto *P = Object->get("params")) + Params = std::move(*P); + + auto I = Handlers.find(*Method); + auto &Handler = I != Handlers.end() ? I->second : UnknownHandler; + + // Create a Context that contains request information. + WithContextValue WithRequestOut(RequestOut, &Out); + llvm::Optional WithID; + if (ID) + WithID.emplace(RequestID, *ID); + + // Create a tracing Span covering the whole request lifetime. + trace::Span Tracer(*Method); + if (ID) + SPAN_ATTACH(Tracer, "ID", *ID); + SPAN_ATTACH(Tracer, "Params", Params); + + // Stash a reference to the span args, so later calls can add metadata. + WithContext WithRequestSpan(RequestSpan::stash(Tracer)); + Handler(std::move(Params)); + return true; +} + +void XPCLSPOutput::resetClientConnection(xpc_connection_t newClientConnection) { + clientConnection = newClientConnection; +} + +void XPCLSPOutput::writeMessage(const json::Value &Message) { + xpc_object_t response = jsonToXpc(Message); + xpc_connection_send_message(clientConnection, response); + xpc_release(response); +} + +void XPCLSPOutput::log(Logger::Level Level, + const llvm::formatv_object_base &Message) { + if (Level < MinLevel) + return; + llvm::sys::TimePoint<> Timestamp = std::chrono::system_clock::now(); + trace::log(Message); + std::lock_guard Guard(LogStreamMutex); + Logs << llvm::formatv("{0}[{1:%H:%M:%S.%L}] {2}\n", indicator(Level), + Timestamp, Message); + Logs.flush(); +} + +void XPCLSPOutput::mirrorInput(const Twine &Message) { + if (!InputMirror) + return; + + *InputMirror << Message; + InputMirror->flush(); +} + +// C "closure" +namespace { + XPCDispatcher *DispatcherPtr = nullptr; + bool *IsDonePtr = nullptr; + XPCLSPOutput *OutPtr = nullptr; + bool HasTransactionBeginBeenCalled = false; + + void connection_handler(xpc_connection_t clientConnection) { + + xpc_connection_set_target_queue(clientConnection, dispatch_get_main_queue()); + + if(!HasTransactionBeginBeenCalled) { + // man xpc_main + // "If the service remains idle after a period of inactivity (defined by + // the system), xpc_main() will exit the process." + // ... + // "Services may extend the default behavior using xpc_transaction_begin()..." + xpc_transaction_begin(); + HasTransactionBeginBeenCalled = true; + } + + OutPtr->resetClientConnection(clientConnection); + + xpc_connection_set_event_handler( + clientConnection, + ^(xpc_object_t message) { + if (message == XPC_ERROR_CONNECTION_INVALID) { + // connection is being terminated + log("Received XPC_ERROR_CONNECTION_INVALID message - returning from the event_handler."); + return; + } + + if (xpc_get_type(message) != XPC_TYPE_DICTIONARY) { + log("Received XPC message of unknown type - returning from the event_handler."); + return; + } + + const json::Value Doc = xpcToJson(message); + + vlog("<<< {0:2}\n", Doc); + OutPtr->mirrorInput( + llvm::formatv("{1}", Doc)); + + if (DispatcherPtr->call(Doc, *OutPtr)) { + if (*IsDonePtr) { + log("IsDonePtr == true. Returning from the event_handler.\n"); + xpc_connection_cancel(xpc_dictionary_get_remote_connection(message)); + } + } else + log("JSON dispatch failed!\n"); + } + ); + + xpc_connection_resume(clientConnection); + } +} + +void clangd::runXPCServerLoop(XPCDispatcher &Dispatcher, XPCLSPOutput &Out, + bool &IsDone) { + DispatcherPtr = &Dispatcher; + IsDonePtr = &IsDone; + OutPtr = &Out; + + xpc_main(connection_handler); +} Index: xpc/cmake/Info.plist.in =================================================================== --- /dev/null +++ xpc/cmake/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${CLANGD_XPC_FRAMEWORK_NAME} + CFBundleIconFile + + CFBundleIdentifier + org.llvm.${CLANGD_XPC_FRAMEWORK_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${CLANGD_XPC_FRAMEWORK_NAME} + CFBundlePackageType + FMWK + CFBundleSignature + ???? + CFBundleVersion + + CFBundleShortVersionString + 1.0 + CSResourcesFileMapped + + + Index: xpc/cmake/XPCServiceInfo.plist.in =================================================================== --- /dev/null +++ xpc/cmake/XPCServiceInfo.plist.in @@ -0,0 +1,30 @@ + + + + + CFBundleExecutable + ${CLANGD_XPC_SERVICE_NAME} + CFBundleIdentifier + ${CLANGD_XPC_SERVICE_BUNDLE_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${CLANGD_XPC_SERVICE_NAME} + CFBundlePackageType + XPC! + CFBundleVersion + + CFBundleShortVersionString + 1.0 + XPCService + + ServiceType + Application + EnvironmentVariables + + CLANGD_AS_XPC_SERVICE + 1 + + + + Index: xpc/cmake/modules/CreateClangdXPCFramework.cmake =================================================================== --- /dev/null +++ xpc/cmake/modules/CreateClangdXPCFramework.cmake @@ -0,0 +1,73 @@ +# Creates the ClangdXPC framework. +macro(create_clangd_xpc_framework target name) + set(CLANGD_FRAMEWORK_LOCATION "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${name}.framework") + set(CLANGD_FRAMEWORK_OUT_LOCATION "${CLANGD_FRAMEWORK_LOCATION}/Versions/A") + + # Create the framework info PLIST. + set(CLANGD_XPC_FRAMEWORK_NAME "${name}") + configure_file( + "${CLANGD_XPC_SOURCE_DIR}/cmake/Info.plist.in" + "${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist") + + set(CLANGD_XPC_SERVICE_NAME "clangd") + set(CLANGD_XPC_SERVICE_OUT_LOCATION + "${CLANGD_FRAMEWORK_OUT_LOCATION}/XPCServices/${CLANGD_XPC_SERVICE_NAME}.xpc/Contents") + + # Create the XPC service info PLIST. + set(CLANGD_XPC_SERVICE_BUNDLE_NAME "org.llvm.${CLANGD_XPC_SERVICE_NAME}") + configure_file( + "${CLANGD_XPC_SOURCE_DIR}/cmake/XPCServiceInfo.plist.in" + "${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist") + + # Create the custom command + add_custom_command(OUTPUT ${CLANGD_FRAMEWORK_LOCATION} + # Copy the PLIST. + COMMAND ${CMAKE_COMMAND} -E copy + "${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist" + "${CLANGD_FRAMEWORK_OUT_LOCATION}/Resources/Info.plist" + + # Copy the framework binary. + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${target}.dylib" + "${CLANGD_FRAMEWORK_OUT_LOCATION}/${name}" + + # Copy the XPC Service PLIST. + COMMAND ${CMAKE_COMMAND} -E copy + "${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist" + "${CLANGD_XPC_SERVICE_OUT_LOCATION}/Info.plist" + + # Copy the Clangd binary. + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/clangd" + "${CLANGD_XPC_SERVICE_OUT_LOCATION}/MacOS/clangd" + + COMMAND ${CMAKE_COMMAND} -E create_symlink "A" + "${CLANGD_FRAMEWORK_LOCATION}/Versions/Current" + + COMMAND ${CMAKE_COMMAND} -E create_symlink + "Versions/Current/Resources" + "${CLANGD_FRAMEWORK_LOCATION}/Resources" + + COMMAND ${CMAKE_COMMAND} -E create_symlink + "Versions/Current/XPCServices" + "${CLANGD_FRAMEWORK_LOCATION}/XPCServices" + + COMMAND ${CMAKE_COMMAND} -E create_symlink + "Versions/Current/${name}" + "${CLANGD_FRAMEWORK_LOCATION}/${name}" + + DEPENDS + "${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist" + "${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist" + clangd + COMMENT "Creating ClangdXPC framework" + VERBATIM + ) + + add_custom_target( + ClangdXPC + DEPENDS + ${target} + ${CLANGD_FRAMEWORK_LOCATION} + ) +endmacro(create_clangd_xpc_framework) Index: xpc/framework/CMakeLists.txt =================================================================== --- /dev/null +++ xpc/framework/CMakeLists.txt @@ -0,0 +1,9 @@ + +set(SOURCES + ClangdXPC.cpp) +add_clang_library(ClangdXPCLib SHARED + ${SOURCES} + DEPENDS + clangd +) +create_clangd_xpc_framework(ClangdXPCLib "ClangdXPC") Index: xpc/framework/ClangdXPC.cpp =================================================================== --- /dev/null +++ xpc/framework/ClangdXPC.cpp @@ -0,0 +1,5 @@ + +/// Returns the bundle identifier of the Clangd XPC service. +extern "C" const char *clangd_xpc_get_bundle_identifier() { + return "org.llvm.clangd"; +} Index: xpc/test-client/CMakeLists.txt =================================================================== --- /dev/null +++ xpc/test-client/CMakeLists.txt @@ -0,0 +1,26 @@ +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/../../ +) + +add_clang_tool( + clangd-xpc-test-client + ClangdXPCTestClient.cpp + + DEPENDS ClangdXPC +) + +set(LLVM_LINK_COMPONENTS + support +) + +target_link_libraries(clangd-xpc-test-client + PRIVATE + clangBasic + clangDaemon + clangFormat + clangFrontend + clangSema + clangTooling + clangToolingCore + clangdXpcJsonConversions +) Index: xpc/test-client/ClangdXPCTestClient.cpp =================================================================== --- /dev/null +++ xpc/test-client/ClangdXPCTestClient.cpp @@ -0,0 +1,96 @@ +#include "xpc/Conversion.h" +#include "clang/Basic/LLVM.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/LineIterator.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include +#include + +typedef const char *(*clangd_xpc_get_bundle_identifier_t)(void); + +using namespace llvm; +using namespace clang; + +std::string getLibraryPath() { + Dl_info info; + if (dladdr((void *)(uintptr_t)getLibraryPath, &info) == 0) + llvm_unreachable("Call to dladdr() failed"); + llvm::SmallString<128> LibClangPath; + LibClangPath = llvm::sys::path::parent_path( + llvm::sys::path::parent_path(info.dli_fname)); + llvm::sys::path::append(LibClangPath, "lib", "ClangdXPC.framework", + "ClangdXPC"); + return LibClangPath.str(); +} + +static void dumpXPCObject(xpc_object_t Object, llvm::raw_ostream &OS) { + xpc_type_t Type = xpc_get_type(Object); + if (Type == XPC_TYPE_DICTIONARY) { + json::Value Json = clang::clangd::xpcToJson(Object); + OS << Json; + } else { + OS << ""; + } +} + +int main(int argc, char *argv[]) { + // Open the ClangdXPC dylib in the framework. + std::string LibPath = getLibraryPath(); + void *dlHandle = dlopen(LibPath.c_str(), RTLD_LOCAL | RTLD_FIRST); + if (!dlHandle) + return 1; + + // Lookup the XPC service bundle name, and launch it. + clangd_xpc_get_bundle_identifier_t clangd_xpc_get_bundle_identifier = + (clangd_xpc_get_bundle_identifier_t)dlsym( + dlHandle, "clangd_xpc_get_bundle_identifier"); + xpc_connection_t conn = + xpc_connection_create(clangd_xpc_get_bundle_identifier(), dispatch_get_main_queue()); + + // Dump the XPC events. + xpc_connection_set_event_handler(conn, ^(xpc_object_t event) { + if (event == XPC_ERROR_CONNECTION_INVALID) { + llvm::errs() << "Received XPC_ERROR_CONNECTION_INVALID."; + exit(EXIT_SUCCESS); + } + if (event == XPC_ERROR_CONNECTION_INTERRUPTED) { + llvm::errs() << "Received XPC_ERROR_CONNECTION_INTERRUPTED."; + exit(EXIT_SUCCESS); + } + + dumpXPCObject(event, llvm::outs()); + llvm::outs() << "\n"; + }); + + xpc_connection_resume(conn); + + // Read the input to determine the things to send to clangd. + llvm::ErrorOr> Stdin = + llvm::MemoryBuffer::getSTDIN(); + if (!Stdin) { + llvm::errs() << "Failed to get STDIN!\n"; + return 1; + } + for (llvm::line_iterator It(**Stdin, /*SkipBlanks=*/true, + /*CommentMarker=*/'#'); + !It.is_at_eof(); ++It) { + StringRef Line = *It; + if (auto Request = json::parse(Line)) { + xpc_object_t Object = clangd::jsonToXpc(*Request); + xpc_connection_send_message(conn, Object); + } else { + llvm::errs() << llvm::Twine("JSON parse error: ") + << llvm::toString(Request.takeError()); + return 1; + } + } + + dispatch_main(); + + // dispatch_main() doesn't return + return EXIT_FAILURE; +}