sammccall updated this revision to Diff 169807.
sammccall added a comment.

Add unit test for JSON transport.


Repository:
  rCTE Clang Tools Extra

https://reviews.llvm.org/D53286

Files:
  clangd/CMakeLists.txt
  clangd/ClangdLSPServer.cpp
  clangd/ClangdLSPServer.h
  clangd/JSONRPCDispatcher.cpp
  clangd/JSONRPCDispatcher.h
  clangd/JSONTransport.cpp
  clangd/Protocol.cpp
  clangd/Protocol.h
  clangd/ProtocolHandlers.cpp
  clangd/Transport.h
  clangd/tool/ClangdMain.cpp
  test/clangd/compile-commands-path-in-initialize.test
  test/clangd/compile-commands-path.test
  test/clangd/completion-snippets.test
  test/clangd/completion.test
  test/clangd/crash-non-added-files.test
  test/clangd/execute-command.test
  test/clangd/input-mirror.test
  test/clangd/signature-help.test
  test/clangd/textdocument-didchange-fail.test
  test/clangd/trace.test
  test/clangd/xrefs.test
  unittests/clangd/CMakeLists.txt
  unittests/clangd/JSONTransportTests.cpp

Index: unittests/clangd/JSONTransportTests.cpp
===================================================================
--- /dev/null
+++ unittests/clangd/JSONTransportTests.cpp
@@ -0,0 +1,199 @@
+//===-- JSONTransportTests.cpp  -------------------------------------------===//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+#include "Protocol.h"
+#include "Transport.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <stdio.h>
+
+using namespace llvm;
+namespace clang {
+namespace clangd {
+namespace {
+
+// No fmemopen on windows, so we can't easily run this test.
+#ifndef WIN32
+
+// Fixture takes care of managing the input/output buffers for the transport.
+class JSONTransportTest : public ::testing::Test {
+  std::string InBuf, OutBuf, MirrorBuf;
+  llvm::raw_string_ostream Out, Mirror;
+  std::unique_ptr<FILE, int (*)(FILE *)> In;
+
+protected:
+  JSONTransportTest() : Out(OutBuf), Mirror(MirrorBuf), In(nullptr, nullptr) {}
+
+  template <typename... Args>
+  std::unique_ptr<Transport> transport(std::string InData, bool Pretty,
+                                       JSONStreamStyle Style) {
+    InBuf = std::move(InData);
+    In = {fmemopen(&InBuf[0], InBuf.size(), "r"), &fclose};
+    return newJSONTransport(In.get(), Out, &Mirror, Pretty, Style);
+  }
+
+  std::string input() const { return InBuf; }
+  std::string output() { return Out.str(); }
+  std::string input_mirror() { return Mirror.str(); }
+};
+
+// Echo is a simple server running on a transport:
+//   - logs each message it gets.
+//   - when it gets a call, replies to it
+//   - when it gets a notification for method "call", makes a call on Target
+// Hangs up when it gets an exit notification.
+class Echo : public Transport::MessageHandler {
+  Transport &Target;
+  std::string LogBuf;
+  raw_string_ostream Log;
+
+public:
+  Echo(Transport &Target) : Target(Target), Log(LogBuf) {}
+
+  std::string log() { return Log.str(); }
+
+  bool notify(StringRef Method, json::Value Params) override {
+    Log << "Notification " << Method << ": " << Params << "\n";
+    if (Method == "call")
+      Target.call("echo call", std::move(Params), 42);
+    return Method == "exit";
+  }
+
+  bool call(StringRef Method, json::Value Params, json::Value ID) override {
+    Log << "Call " << Method << "(" << ID << "): " << Params << "\n";
+    if (Method == "err")
+      Target.reply(ID, make_error<LSPError>("trouble at mill", ErrorCode(88)));
+    else
+      Target.reply(ID, std::move(Params));
+    return false;
+  }
+
+  bool reply(json::Value ID, Expected<json::Value> Params) override {
+    if (Params)
+      Log << "Reply(" << ID << "): " << *Params << "\n";
+    else
+      Log << "Reply(" << ID
+          << "): error = " << llvm::toString(Params.takeError()) << "\n";
+    return false;
+  }
+};
+
+std::string trim(StringRef S) { return S.trim().str(); }
+
+// Runs an Echo session using the standard JSON-RPC format we use in production.
+TEST_F(JSONTransportTest, StandardDense) {
+  auto T = transport(
+      "Content-Length: 52\r\n\r\n"
+      R"({"jsonrpc": "2.0", "method": "call", "params": 1234})"
+      "Content-Length: 46\r\n\r\n"
+      R"({"jsonrpc": "2.0", "id": 1234, "result": 5678})"
+      "Content-Length: 67\r\n\r\n"
+      R"({"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"})"
+      "Content-Length: 73\r\n\r\n"
+      R"({"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}})"
+      "Content-Length: 68\r\n\r\n"
+      R"({"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"})"
+      "Content-Length: 36\r\n\r\n"
+      R"({"jsonrpc": "2.0", "method": "exit"})",
+      /*Pretty=*/false, JSONStreamStyle::Standard);
+  Echo E(*T);
+  auto Err = T->loop(E);
+  EXPECT_FALSE(bool(Err)) << llvm::toString(std::move(Err));
+
+  EXPECT_EQ(trim(E.log()), trim(R"(
+Notification call: 1234
+Reply(1234): 5678
+Call foo("abcd"): "efgh"
+Reply("xyz"): error = 99: bad!
+Call err("wxyz"): "boom!"
+Notification exit: null
+  )"));
+  EXPECT_EQ(
+      trim(output()),
+      "Content-Length: 60\r\n\r\n"
+      R"({"id":42,"jsonrpc":"2.0","method":"echo call","params":1234})"
+      "Content-Length: 45\r\n\r\n"
+      R"({"id":"abcd","jsonrpc":"2.0","result":"efgh"})"
+      "Content-Length: 77\r\n\r\n"
+      R"({"error":{"code":88,"message":"trouble at mill"},"id":"wxyz","jsonrpc":"2.0"})");
+  EXPECT_EQ(trim(input_mirror()), trim(input()));
+}
+
+// Runs an Echo session using the "delimited" input and pretty-printed output
+// that we use in lit tests.
+TEST_F(JSONTransportTest, DelimitedPretty) {
+  auto T = transport(R"jsonrpc(
+{"jsonrpc": "2.0", "method": "call", "params": 1234}
+---
+{"jsonrpc": "2.0", "id": 1234, "result": 5678}
+---
+{"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"}
+---
+{"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}}
+---
+{"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"}
+---
+{"jsonrpc": "2.0", "method": "exit"}
+  )jsonrpc",
+                     /*Pretty=*/true, JSONStreamStyle::Delimited);
+  Echo E(*T);
+  auto Err = T->loop(E);
+  EXPECT_FALSE(bool(Err)) << llvm::toString(std::move(Err));
+
+  EXPECT_EQ(trim(E.log()), trim(R"(
+Notification call: 1234
+Reply(1234): 5678
+Call foo("abcd"): "efgh"
+Reply("xyz"): error = 99: bad!
+Call err("wxyz"): "boom!"
+Notification exit: null
+  )"));
+  EXPECT_EQ(trim(output()), "Content-Length: 77\r\n\r\n"
+                            R"({
+  "id": 42,
+  "jsonrpc": "2.0",
+  "method": "echo call",
+  "params": 1234
+})"
+                            "Content-Length: 58\r\n\r\n"
+                            R"({
+  "id": "abcd",
+  "jsonrpc": "2.0",
+  "result": "efgh"
+})"
+                            "Content-Length: 105\r\n\r\n"
+                            R"({
+  "error": {
+    "code": 88,
+    "message": "trouble at mill"
+  },
+  "id": "wxyz",
+  "jsonrpc": "2.0"
+})");
+  EXPECT_EQ(trim(input_mirror()), trim(input()));
+}
+
+// IO errors such as EOF ane reported.
+// The only successful return from loop() is if a handler returned true.
+TEST_F(JSONTransportTest, EndOfFile) {
+  auto T = transport("Content-Length: 52\r\n\r\n"
+                     R"({"jsonrpc": "2.0", "method": "call", "params": 1234})",
+                     /*Pretty=*/false, JSONStreamStyle::Standard);
+  Echo E(*T);
+  auto Err = T->loop(E);
+  EXPECT_EQ(trim(E.log()), "Notification call: 1234");
+  EXPECT_TRUE(bool(Err)); // Ran into EOF with no handler signalling done.
+  consumeError(std::move(Err));
+  EXPECT_EQ(trim(input_mirror()), trim(input()));
+}
+
+#endif
+
+} // namespace
+} // namespace clangd
+} // namespace clang
Index: unittests/clangd/CMakeLists.txt
===================================================================
--- unittests/clangd/CMakeLists.txt
+++ unittests/clangd/CMakeLists.txt
@@ -26,6 +26,7 @@
   GlobalCompilationDatabaseTests.cpp
   HeadersTests.cpp
   IndexTests.cpp
+  JSONTransportTests.cpp
   QualityTests.cpp
   RIFFTests.cpp
   SerializationTests.cpp
Index: test/clangd/xrefs.test
===================================================================
--- test/clangd/xrefs.test
+++ test/clangd/xrefs.test
@@ -55,3 +55,5 @@
 # CHECK-NEXT: ]
 ---
 {"jsonrpc":"2.0","id":10000,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/trace.test
===================================================================
--- test/clangd/trace.test
+++ test/clangd/trace.test
@@ -21,3 +21,5 @@
 # CHECK: },
 ---
 {"jsonrpc":"2.0","id":5,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/textdocument-didchange-fail.test
===================================================================
--- test/clangd/textdocument-didchange-fail.test
+++ test/clangd/textdocument-didchange-fail.test
@@ -35,3 +35,5 @@
 # CHECK-NEXT:}
 ---
 {"jsonrpc":"2.0","id":4,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/signature-help.test
===================================================================
--- test/clangd/signature-help.test
+++ test/clangd/signature-help.test
@@ -23,3 +23,5 @@
 # CHECK-NEXT: }
 ---
 {"jsonrpc":"2.0","id":100000,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/input-mirror.test
===================================================================
--- test/clangd/input-mirror.test
+++ test/clangd/input-mirror.test
@@ -12,3 +12,6 @@
 Content-Length: 44
 
 {"jsonrpc":"2.0","id":3,"method":"shutdown"}
+Content-Length: 33
+
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/execute-command.test
===================================================================
--- test/clangd/execute-command.test
+++ test/clangd/execute-command.test
@@ -62,3 +62,5 @@
 {"jsonrpc":"2.0","id":9,"method":"workspace/executeCommand","params":{"arguments":[{"custom":"foo", "changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":{"start":{"line":0,"character":37},"end":{"line":0,"character":37}},"newText":")"}]}}],"command":"clangd.applyFix"}}
 ---
 {"jsonrpc":"2.0","id":3,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/crash-non-added-files.test
===================================================================
--- test/clangd/crash-non-added-files.test
+++ test/clangd/crash-non-added-files.test
@@ -32,3 +32,5 @@
 {"jsonrpc":"2.0","id":6,"method":"shutdown"}
 ---
 {"jsonrpc":"2.0","method":"exit"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/completion.test
===================================================================
--- test/clangd/completion.test
+++ test/clangd/completion.test
@@ -68,3 +68,5 @@
 # CHECK-NEXT:  ]
 ---
 {"jsonrpc":"2.0","id":4,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/completion-snippets.test
===================================================================
--- test/clangd/completion-snippets.test
+++ test/clangd/completion-snippets.test
@@ -52,3 +52,5 @@
 # CHECK-NEXT:  }
 ---
 {"jsonrpc":"2.0","id":4,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/compile-commands-path.test
===================================================================
--- test/clangd/compile-commands-path.test
+++ test/clangd/compile-commands-path.test
@@ -40,3 +40,5 @@
 # CHECK-NEXT:         "message": "MACRO is two",
 ---
 {"jsonrpc":"2.0","id":10000,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: test/clangd/compile-commands-path-in-initialize.test
===================================================================
--- test/clangd/compile-commands-path-in-initialize.test
+++ test/clangd/compile-commands-path-in-initialize.test
@@ -33,3 +33,5 @@
 # CHECK-NEXT:         "message": "MACRO is two",
 ---
 {"jsonrpc":"2.0","id":10000,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
Index: clangd/tool/ClangdMain.cpp
===================================================================
--- clangd/tool/ClangdMain.cpp
+++ clangd/tool/ClangdMain.cpp
@@ -253,11 +253,8 @@
   // Use buffered stream to stderr (we still flush each log message). Unbuffered
   // stream can cause significant (non-deterministic) latency for the logger.
   llvm::errs().SetBuffered();
-  JSONOutput Out(llvm::outs(), llvm::errs(), LogLevel,
-                 InputMirrorStream ? InputMirrorStream.getPointer() : nullptr,
-                 PrettyPrint);
-
-  clangd::LoggingSession LoggingSession(Out);
+  JSONOutput Logger(llvm::errs(), LogLevel);
+  clangd::LoggingSession LoggingSession(Logger);
 
   // If --compile-commands-dir arg was invoked, check value and override default
   // path.
@@ -316,12 +313,16 @@
   CCOpts.AllScopes = AllScopesCompletion;
 
   // Initialize and run ClangdLSPServer.
+  // Change stdin to binary to not lose \r\n on windows.
+  llvm::sys::ChangeStdinToBinary();
+  auto Transport = newJSONTransport(
+      stdin, llvm::outs(),
+      InputMirrorStream ? InputMirrorStream.getPointer() : nullptr, PrettyPrint,
+      InputStyle);
   ClangdLSPServer LSPServer(
-      Out, CCOpts, CompileCommandsDirPath,
+      *Transport, CCOpts, CompileCommandsDirPath,
       /*ShouldUseInMemoryCDB=*/CompileArgsFrom == LSPCompileArgs, Opts);
   constexpr int NoShutdownRequestErrorCode = 1;
   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;
+  return LSPServer.run() ? 0 : NoShutdownRequestErrorCode;
 }
Index: clangd/Transport.h
===================================================================
--- /dev/null
+++ clangd/Transport.h
@@ -0,0 +1,83 @@
+//===--- Transport.h - sending and receiving LSP messages -------*- C++ -*-===//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+//
+// The language server protocol is usually implemented by writing messages as
+// JSON-RPC over the stdin/stdout of a subprocess. However other communications
+// mechanisms are possible, such as XPC on mac.
+//
+// The Transport interface allows the mechanism to be replaced, and the JSONRPC
+// Transport is the standard implementation.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_TRANSPORT_H_
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_TRANSPORT_H_
+
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace clang {
+namespace clangd {
+
+// A transport is responsible for maintaining the connection to a client
+// application, and reading/writing structured messages to it.
+//
+// Transports have limited thread safety requirements:
+//  - messages will not be sent concurrently
+//  - messages MAY be sent while loop() is reading, or its callback is active
+class Transport {
+public:
+  virtual ~Transport() = default;
+
+  // Called by Clangd to send messages to the client.
+  virtual void notify(llvm::StringRef Method, llvm::json::Value Params) = 0;
+  virtual void call(llvm::StringRef Method, llvm::json::Value Params,
+                    llvm::json::Value ID) = 0;
+  virtual void reply(llvm::json::Value ID,
+                     llvm::Expected<llvm::json::Value> Result) = 0;
+
+  // Implemented by Clangd to handle incoming messages. (See loop() below).
+  class MessageHandler {
+  public:
+    virtual ~MessageHandler() = default;
+    virtual bool notify(llvm::StringRef Method, llvm::json::Value) = 0;
+    virtual bool call(llvm::StringRef Method, llvm::json::Value Params,
+                      llvm::json::Value ID) = 0;
+    virtual bool reply(llvm::json::Value ID,
+                       llvm::Expected<llvm::json::Value> Result) = 0;
+  };
+  // Called by Clangd to receive messages from the client.
+  // The transport should in turn invoke the handler to process messages.
+  // If handler returns true, the transport should immedately return success.
+  // (This is used to implement the `exit` notification).
+  // Otherwise, it returns an error when the transport becomes unusable.
+  virtual llvm::Error loop(MessageHandler &) = 0;
+};
+
+// Controls the way JSON-RPC messages are encoded (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
+};
+
+// Returns a Transport that speaks JSON-RPC over a pair of streams.
+// The input stream must be opened in binary mode.
+// If InMirror is set, data read will be echoed to it.
+std::unique_ptr<Transport>
+newJSONTransport(std::FILE *In, llvm::raw_ostream &Out,
+                 llvm::raw_ostream *InMirror, bool Pretty,
+                 JSONStreamStyle = JSONStreamStyle::Standard);
+
+} // namespace clangd
+} // namespace clang
+
+#endif
Index: clangd/ProtocolHandlers.cpp
===================================================================
--- clangd/ProtocolHandlers.cpp
+++ clangd/ProtocolHandlers.cpp
@@ -35,6 +35,7 @@
       } else {
         elog("Failed to decode {0} request.", Method);
       }
+      return Method == "exit";
     });
   }
 
Index: clangd/Protocol.h
===================================================================
--- clangd/Protocol.h
+++ clangd/Protocol.h
@@ -48,6 +48,23 @@
   // Defined by the protocol.
   RequestCancelled = -32800,
 };
+// Models an LSP error as an llvm::Error.
+class LSPError : public llvm::ErrorInfo<LSPError> {
+public:
+  std::string Message;
+  ErrorCode Code;
+  static char ID;
+
+  LSPError(std::string Message, ErrorCode Code)
+      : Message(std::move(Message)), Code(Code) {}
+
+  void log(llvm::raw_ostream &OS) const override {
+    OS << int(Code) << ": " << Message;
+  }
+  std::error_code convertToErrorCode() const override {
+    return llvm::inconvertibleErrorCode();
+  }
+};
 
 struct URIForFile {
   URIForFile() = default;
Index: clangd/Protocol.cpp
===================================================================
--- clangd/Protocol.cpp
+++ clangd/Protocol.cpp
@@ -25,6 +25,8 @@
 namespace clangd {
 using namespace llvm;
 
+char LSPError::ID;
+
 URIForFile::URIForFile(std::string AbsPath) {
   assert(llvm::sys::path::is_absolute(AbsPath) && "the path is relative");
   File = std::move(AbsPath);
Index: clangd/JSONTransport.cpp
===================================================================
--- /dev/null
+++ clangd/JSONTransport.cpp
@@ -0,0 +1,293 @@
+//===--- JSONTransport.cpp - sending and receiving LSP messages over JSON -===//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+#include "Logger.h"
+#include "Protocol.h" // For LSPError
+#include "Transport.h"
+#include "llvm/Support/Errno.h"
+
+using namespace llvm;
+namespace clang {
+namespace clangd {
+namespace {
+
+json::Object encodeError(Error E) {
+  ErrorCode Code = ErrorCode::UnknownErrorCode;
+  std::string Msg = toString(handleErrors(std::move(E), [&](const LSPError &L) {
+    Code = L.Code;
+    return make_error<StringError>(L.Message, inconvertibleErrorCode());
+  }));
+  return json::Object{
+      {"message", std::move(Msg)},
+      {"code", int64_t(Code)},
+  };
+}
+
+Error decodeError(const json::Object &O) {
+  std::string Msg = O.getString("message").getValueOr("Unspecified error");
+  if (auto Code = O.getInteger("code"))
+    return make_error<LSPError>(std::move(Msg), ErrorCode(*Code));
+  return make_error<StringError>(std::move(Msg), inconvertibleErrorCode());
+}
+
+class JSONTransport : public Transport {
+public:
+  JSONTransport(std::FILE *In, llvm::raw_ostream &Out,
+                llvm::raw_ostream *InMirror, bool Pretty, JSONStreamStyle Style)
+      : In(In), Out(Out), InMirror(InMirror ? *InMirror : nulls()),
+        Pretty(Pretty), Style(Style) {}
+
+  void notify(StringRef Method, json::Value Params) override {
+    sendMessage(json::Object{
+        {"jsonrpc", "2.0"},
+        {"method", Method},
+        {"params", std::move(Params)},
+    });
+  }
+  void call(StringRef Method, json::Value Params, json::Value ID) override {
+    sendMessage(json::Object{
+        {"jsonrpc", "2.0"},
+        {"id", std::move(ID)},
+        {"method", Method},
+        {"params", std::move(Params)},
+    });
+  }
+  void reply(json::Value ID, Expected<json::Value> Result) override {
+    if (Result) {
+      sendMessage(json::Object{
+          {"jsonrpc", "2.0"},
+          {"id", std::move(ID)},
+          {"result", std::move(*Result)},
+      });
+    } else {
+      sendMessage(json::Object{
+          {"jsonrpc", "2.0"},
+          {"id", std::move(ID)},
+          {"error", encodeError(Result.takeError())},
+      });
+    }
+  }
+
+  Error loop(MessageHandler &Handler) override {
+    while (!feof(In)) {
+      if (ferror(In))
+        return errorCodeToError(std::error_code(errno, std::system_category()));
+      if (auto JSON = readRawMessage()) {
+        if (auto Doc = json::parse(*JSON)) {
+          vlog(Pretty ? "<<< {0:2}\n" : "<<< {0}\n", *Doc);
+          if (handleMessage(std::move(*Doc), Handler))
+            return Error::success();
+        } else {
+          // Parse error. Log the raw message.
+          vlog("<<< {0}\n", *JSON);
+          elog("JSON parse error: {0}", llvm::toString(Doc.takeError()));
+        }
+      }
+    }
+    return errorCodeToError(std::make_error_code(std::errc::io_error));
+  }
+
+private:
+  // Dispatches incoming message to Handler notify/call/reply.
+  bool handleMessage(llvm::json::Value Message, MessageHandler &Handler);
+  // Writes outgoing message to Out stream.
+  void sendMessage(llvm::json::Value Message) {
+    std::string S;
+    llvm::raw_string_ostream OS(S);
+    OS << llvm::formatv(Pretty ? "{0:2}" : "{0}", Message);
+    OS.flush();
+    Out << "Content-Length: " << S.size() << "\r\n\r\n" << S;
+    Out.flush();
+    vlog(">>> {0}\n", S);
+  }
+
+  // Read raw string messages from input stream.
+  llvm::Optional<std::string> readRawMessage() {
+    return Style == JSONStreamStyle::Delimited ? readDelimitedMessage()
+                                               : readStandardMessage();
+  }
+  llvm::Optional<std::string> readDelimitedMessage();
+  llvm::Optional<std::string> readStandardMessage();
+
+  std::FILE *In;
+  llvm::raw_ostream &Out;
+  llvm::raw_ostream &InMirror;
+  bool Pretty;
+  JSONStreamStyle Style;
+};
+
+bool JSONTransport::handleMessage(llvm::json::Value Message,
+                                  MessageHandler &Handler) {
+  // Message must be an object with "jsonrpc":"2.0".
+  auto *Object = Message.getAsObject();
+  if (!Object || Object->getString("jsonrpc") != Optional<StringRef>("2.0")) {
+    elog("Not a JSON-RPC message: {0:2}", Message);
+    return false;
+  }
+  // ID may be any JSON value. If absent, this is a notification.
+  llvm::Optional<json::Value> ID;
+  if (auto *I = Object->get("id"))
+    ID = std::move(*I);
+  auto Method = Object->getString("method");
+  if (!Method) { // This is a response.
+    if (!ID) {
+      elog("No method and no response ID: {0:2}", Message);
+      return false;
+    }
+    if (auto *Err = Object->getObject("error"))
+      return Handler.reply(std::move(*ID), decodeError(*Err));
+    // Result should be given, use null if not.
+    json::Value Result = nullptr;
+    if (auto *R = Object->get("result"))
+      Result = std::move(*R);
+    return Handler.reply(std::move(*ID), std::move(Result));
+  }
+  // Params should be given, use null if not.
+  json::Value Params = nullptr;
+  if (auto *P = Object->get("params"))
+    Params = std::move(*P);
+
+  if (ID)
+    return Handler.call(*Method, std::move(Params), std::move(*ID));
+  else
+    return Handler.notify(*Method, std::move(Params));
+}
+
+// Tries to read a line up to and including \n.
+// If failing, feof() or ferror() will be set.
+bool readLine(std::FILE *In, std::string &Out) {
+  static constexpr int BufSize = 1024;
+  size_t Size = 0;
+  Out.clear();
+  for (;;) {
+    Out.resize(Size + BufSize);
+    // Handle EINTR which is sent when a debugger attaches on some platforms.
+    if (!llvm::sys::RetryAfterSignal(nullptr, ::fgets, &Out[Size], BufSize, In))
+      return false;
+    clearerr(In);
+    // If the line contained null bytes, anything after it (including \n) will
+    // be ignored. Fortunately this is not a legal header or JSON.
+    size_t Read = std::strlen(&Out[Size]);
+    if (Read > 0 && Out[Size + Read - 1] == '\n') {
+      Out.resize(Size + Read);
+      return true;
+    }
+    Size += Read;
+  }
+}
+
+// Returns None when:
+//  - ferror() or feof() are set.
+//  - Content-Length is missing or empty (protocol error)
+llvm::Optional<std::string> JSONTransport::readStandardMessage() {
+  // 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;
+  std::string Line;
+  while (true) {
+    if (feof(In) || ferror(In) || !readLine(In, Line))
+      return llvm::None;
+    InMirror << Line;
+
+    llvm::StringRef LineRef(Line);
+
+    // We allow comments in headers. Technically this isn't part
+
+    // of the LSP specification, but makes writing tests easier.
+    if (LineRef.startswith("#"))
+      continue;
+
+    // Content-Length is a mandatory header, and the only one we handle.
+    if (LineRef.consume_front("Content-Length: ")) {
+      if (ContentLength != 0) {
+        elog("Warning: Duplicate Content-Length header received. "
+             "The previous value for this message ({0}) was ignored.",
+             ContentLength);
+      }
+      llvm::getAsUnsignedInteger(LineRef.trim(), 0, ContentLength);
+      continue;
+    } else if (!LineRef.trim().empty()) {
+      // It's another header, ignore it.
+      continue;
+    } else {
+      // An empty line indicates the end of headers.
+      // Go ahead and read the JSON.
+      break;
+    }
+  }
+
+  // The fuzzer likes crashing us by sending "Content-Length: 9999999999999999"
+  if (ContentLength > 1 << 30) { // 1024M
+    elog("Refusing to read message with long Content-Length: {0}. "
+         "Expect protocol errors",
+         ContentLength);
+    return llvm::None;
+  }
+  if (ContentLength == 0) {
+    log("Warning: Missing Content-Length header, or zero-length message.");
+    return llvm::None;
+  }
+
+  std::string JSON(ContentLength, '\0');
+  for (size_t Pos = 0, Read; Pos < ContentLength; Pos += Read) {
+    // Handle EINTR which is sent when a debugger attaches on some platforms.
+    Read = llvm::sys::RetryAfterSignal(0u, ::fread, &JSON[Pos], 1,
+                                       ContentLength - Pos, In);
+    if (Read == 0) {
+      elog("Input was aborted. Read only {0} bytes of expected {1}.", Pos,
+           ContentLength);
+      return llvm::None;
+    }
+    InMirror << StringRef(&JSON[Pos], Read);
+    clearerr(In); // If we're done, the error was transient. If we're not done,
+                  // either it was transient or we'll see it again on retry.
+    Pos += Read;
+  }
+  return std::move(JSON);
+}
+
+// For lit tests we support a simplified syntax:
+// - messages are delimited by '---' on a line by itself
+// - lines starting with # are ignored.
+// This is a testing path, so favor simplicity over performance here.
+// When returning None, feof() or ferror() will be set.
+llvm::Optional<std::string> JSONTransport::readDelimitedMessage() {
+  std::string JSON;
+  std::string Line;
+  while (readLine(In, Line)) {
+    InMirror << Line;
+    auto LineRef = llvm::StringRef(Line).trim();
+    if (LineRef.startswith("#")) // comment
+      continue;
+
+    // found a delimiter
+    if (LineRef.rtrim() == "---")
+      break;
+
+    JSON += Line;
+  }
+
+  if (ferror(In)) {
+    elog("Input error while reading message!");
+    return llvm::None;
+  }
+  return std::move(JSON); // Including at EOF
+}
+
+} // namespace
+
+std::unique_ptr<Transport> newJSONTransport(std::FILE *In,
+                                            llvm::raw_ostream &Out,
+                                            llvm::raw_ostream *InMirror,
+                                            bool Pretty,
+                                            JSONStreamStyle Style) {
+  return llvm::make_unique<JSONTransport>(In, Out, InMirror, Pretty, Style);
+}
+
+} // namespace clangd
+} // namespace clang
Index: clangd/JSONRPCDispatcher.h
===================================================================
--- clangd/JSONRPCDispatcher.h
+++ clangd/JSONRPCDispatcher.h
@@ -14,6 +14,7 @@
 #include "Logger.h"
 #include "Protocol.h"
 #include "Trace.h"
+#include "Transport.h"
 #include "clang/Basic/LLVM.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringMap.h"
@@ -24,37 +25,19 @@
 namespace clang {
 namespace clangd {
 
-/// Encapsulates output and logs streams and provides thread-safe access to
-/// them.
+// Logs to an output stream, such as stderr.
+// TODO: rename and move.
 class JSONOutput : public Logger {
-  // FIXME(ibiryukov): figure out if we can shrink the public interface of
-  // JSONOutput now that we pass Context everywhere.
 public:
-  JSONOutput(llvm::raw_ostream &Outs, llvm::raw_ostream &Logs,
-             Logger::Level MinLevel, llvm::raw_ostream *InputMirror = nullptr,
-             bool Pretty = false)
-      : Pretty(Pretty), MinLevel(MinLevel), Outs(Outs), Logs(Logs),
-        InputMirror(InputMirror) {}
-
-  /// Emit a JSONRPC message.
-  void writeMessage(const llvm::json::Value &Result);
+  JSONOutput(llvm::raw_ostream &Logs, Logger::Level MinLevel)
+      : MinLevel(MinLevel), Logs(Logs) {}
 
   /// 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 of JSONOutput, mirrorInput is not thread-safe.
-  void mirrorInput(const Twine &Message);
-
-  // Whether output should be pretty-printed.
-  const bool Pretty;
-
 private:
   Logger::Level MinLevel;
-  llvm::raw_ostream &Outs;
   llvm::raw_ostream &Logs;
-  llvm::raw_ostream *InputMirror;
 
   std::mutex StreamMutex;
 };
@@ -81,26 +64,39 @@
 ///
 /// The `$/cancelRequest` notification is handled by the dispatcher itself.
 /// It marks the matching request as cancelled, if it's still running.
-class JSONRPCDispatcher {
+class JSONRPCDispatcher : private Transport::MessageHandler {
 public:
   /// A handler responds to requests for a particular method name.
+  /// It returns true if the server should now shut down.
   ///
   /// JSONRPCDispatcher will mark the handler's context as cancelled if a
   /// matching cancellation request is received. Handlers are encouraged to
   /// check for cancellation and fail quickly in this case.
-  using Handler = std::function<void(const llvm::json::Value &)>;
+  using Handler = std::function<bool(const llvm::json::Value &)>;
 
   /// Create a new JSONRPCDispatcher. UnknownHandler is called when an unknown
   /// method is received.
   JSONRPCDispatcher(Handler 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 llvm::json::Value &Message, JSONOutput &Out);
+  /// Parses input queries from LSP client (coming from \p In) and runs call
+  /// method for each query.
+  ///
+  /// Input stream(\p In) must be opened in binary mode to avoid
+  /// preliminary 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.
+  llvm::Error runLanguageServerLoop(Transport &);
 
 private:
+  bool reply(llvm::json::Value ID,
+             llvm::Expected<llvm::json::Value> Result) override;
+  bool notify(llvm::StringRef Method, llvm::json::Value Message) override;
+  bool call(llvm::StringRef Method, llvm::json::Value Message,
+            llvm::json::Value ID) override;
+
   // Tracking cancellations needs a mutex: handlers may finish on a different
   // thread, and that's when we clean up entries in the map.
   mutable std::mutex RequestCancelersMutex;
@@ -113,25 +109,6 @@
   Handler UnknownHandler;
 };
 
-/// Controls the way JSON-RPC messages are encoded (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
-};
-
-/// Parses input queries from LSP client (coming from \p In) and runs call
-/// method of \p Dispatcher for each query.
-/// After handling each query checks if \p IsDone is set true and exits the loop
-/// if it is.
-/// Input stream(\p In) must be opened in binary mode to avoid preliminary
-/// 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,
-                           JSONStreamStyle InputStyle,
-                           JSONRPCDispatcher &Dispatcher, bool &IsDone);
 } // namespace clangd
 } // namespace clang
 
Index: clangd/JSONRPCDispatcher.cpp
===================================================================
--- clangd/JSONRPCDispatcher.cpp
+++ clangd/JSONRPCDispatcher.cpp
@@ -11,6 +11,7 @@
 #include "Cancellation.h"
 #include "ProtocolHandlers.h"
 #include "Trace.h"
+#include "Transport.h"
 #include "llvm/ADT/ScopeExit.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringExtras.h"
@@ -28,7 +29,7 @@
 
 namespace {
 static Key<json::Value> RequestID;
-static Key<JSONOutput *> RequestOut;
+static Key<Transport *> CurrentTransport;
 
 // When tracing, we trace a request and attach the response in reply().
 // Because the Span isn't available, we find the current request using Context.
@@ -58,23 +59,6 @@
 Key<std::unique_ptr<RequestSpan>> RequestSpan::RSKey;
 } // namespace
 
-void JSONOutput::writeMessage(const json::Value &Message) {
-  std::string S;
-  llvm::raw_string_ostream OS(S);
-  if (Pretty)
-    OS << llvm::formatv("{0:2}", Message);
-  else
-    OS << Message;
-  OS.flush();
-
-  {
-    std::lock_guard<std::mutex> Guard(StreamMutex);
-    Outs << "Content-Length: " << S.size() << "\r\n\r\n" << S;
-    Outs.flush();
-  }
-  vlog(">>> {0}\n", S);
-}
-
 void JSONOutput::log(Logger::Level Level,
                      const llvm::formatv_object_base &Message) {
   if (Level < MinLevel)
@@ -87,14 +71,6 @@
   Logs.flush();
 }
 
-void JSONOutput::mirrorInput(const Twine &Message) {
-  if (!InputMirror)
-    return;
-
-  *InputMirror << Message;
-  InputMirror->flush();
-}
-
 void clangd::reply(json::Value &&Result) {
   auto ID = getRequestId();
   if (!ID) {
@@ -104,12 +80,8 @@
   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)},
-      });
+      .getExisting(CurrentTransport)
+      ->reply(std::move(*ID), std::move(Result));
 }
 
 void clangd::replyError(ErrorCode Code, const llvm::StringRef &Message) {
@@ -122,13 +94,8 @@
   if (auto ID = getRequestId()) {
     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<int>(Code)},
-                                   {"message", Message}}},
-        });
+        .getExisting(CurrentTransport)
+        ->reply(std::move(*ID), make_error<LSPError>(Message, Code));
   }
 }
 
@@ -151,89 +118,71 @@
   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)},
-      });
+      .getExisting(CurrentTransport)
+      ->call(Method, std::move(Params), ID);
 }
 
 JSONRPCDispatcher::JSONRPCDispatcher(Handler UnknownHandler)
     : UnknownHandler(std::move(UnknownHandler)) {
   registerHandler("$/cancelRequest", [this](const json::Value &Params) {
     if (auto *O = Params.getAsObject())
-      if (auto *ID = O->get("id"))
-        return cancelRequest(*ID);
+      if (auto *ID = O->get("id")) {
+        cancelRequest(*ID);
+        return false;
+      }
     log("Bad cancellation request: {0}", Params);
+    return false;
   });
 }
 
 void JSONRPCDispatcher::registerHandler(StringRef Method, Handler H) {
   assert(!Handlers.count(Method) && "Handler already registered!");
   Handlers[Method] = std::move(H);
 }
 
-static void logIncomingMessage(const llvm::Optional<json::Value> &ID,
-                               llvm::Optional<StringRef> Method,
-                               const json::Object *Error) {
-  if (Method) { // incoming request
-    if (ID)     // call
-      log("<-- {0}({1})", *Method, *ID);
-    else // notification
-      log("<-- {0}", *Method);
-  } else if (ID) { // response, ID must be provided
-    if (Error)
-      log("<-- reply({0}) error: {1}", *ID,
-          Error->getString("message").getValueOr("<no message>"));
-    else
-      log("<-- reply({0})", *ID);
-  }
-}
-
-bool JSONRPCDispatcher::call(const json::Value &Message, JSONOutput &Out) {
-  // Message must be an object with "jsonrpc":"2.0".
-  auto *Object = Message.getAsObject();
-  if (!Object || Object->getString("jsonrpc") != Optional<StringRef>("2.0"))
-    return false;
-  // ID may be any JSON value. If absent, this is a notification.
-  llvm::Optional<json::Value> ID;
-  if (auto *I = Object->get("id"))
-    ID = std::move(*I);
-  auto Method = Object->getString("method");
-  logIncomingMessage(ID, Method, Object->getObject("error"));
-  if (!Method) // We only handle incoming requests, and ignore responses.
-    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);
+bool JSONRPCDispatcher::call(StringRef Method, json::Value Params,
+                             json::Value ID) {
+  log("<-- {0}({1})", Method, ID);
+  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<WithContextValue> WithID;
-  if (ID)
-    WithID.emplace(RequestID, *ID);
+  WithContextValue WithID(RequestID, ID);
 
   // Create a tracing Span covering the whole request lifetime.
-  trace::Span Tracer(*Method);
-  if (ID)
-    SPAN_ATTACH(Tracer, "ID", *ID);
+  trace::Span Tracer(Method);
+  SPAN_ATTACH(Tracer, "ID", ID);
   SPAN_ATTACH(Tracer, "Params", Params);
 
-  // Requests with IDs can be canceled by the client. Add cancellation context.
-  llvm::Optional<WithContext> WithCancel;
-  if (ID)
-    WithCancel.emplace(cancelableRequestContext(*ID));
+  // Calls can be canceled by the client. Add cancellation context.
+  WithContext WithCancel(cancelableRequestContext(ID));
+
+  // Stash a reference to the span args, so later calls can add metadata.
+  WithContext WithRequestSpan(RequestSpan::stash(Tracer));
+  return Handler(std::move(Params));
+}
+
+bool JSONRPCDispatcher::notify(StringRef Method, json::Value Params) {
+  log("<-- {0}", Method);
+  auto I = Handlers.find(Method);
+  auto &Handler = I != Handlers.end() ? I->second : UnknownHandler;
+
+  // Create a tracing Span covering the whole request lifetime.
+  trace::Span Tracer(Method);
+  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;
+  return Handler(std::move(Params));
+}
+
+bool JSONRPCDispatcher::reply(json::Value ID, Expected<json::Value> Result) {
+  // We ignore replies, just log them.
+  if (Result)
+    log("<-- reply({0})", ID);
+  else
+    log("<-- reply({0}) error: {1}", ID, llvm::toString(Result.takeError()));
+  return false;
 }
 
 // We run cancelable requests in a context that does two things:
@@ -266,162 +215,17 @@
     It->second.first(); // Invoke the canceler.
 }
 
-// Tries to read a line up to and including \n.
-// If failing, feof() or ferror() will be set.
-static bool readLine(std::FILE *In, std::string &Out) {
-  static constexpr int BufSize = 1024;
-  size_t Size = 0;
-  Out.clear();
-  for (;;) {
-    Out.resize(Size + BufSize);
-    // Handle EINTR which is sent when a debugger attaches on some platforms.
-    if (!llvm::sys::RetryAfterSignal(nullptr, ::fgets, &Out[Size], BufSize, In))
-      return false;
-    clearerr(In);
-    // If the line contained null bytes, anything after it (including \n) will
-    // be ignored. Fortunately this is not a legal header or JSON.
-    size_t Read = std::strlen(&Out[Size]);
-    if (Read > 0 && Out[Size + Read - 1] == '\n') {
-      Out.resize(Size + Read);
-      return true;
-    }
-    Size += Read;
-  }
-}
-
-// Returns None when:
-//  - ferror() or feof() are set.
-//  - Content-Length is missing or empty (protocol error)
-static llvm::Optional<std::string> readStandardMessage(std::FILE *In,
-                                                       JSONOutput &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;
-  std::string Line;
-  while (true) {
-    if (feof(In) || ferror(In) || !readLine(In, Line))
-      return llvm::None;
-
-    Out.mirrorInput(Line);
-    llvm::StringRef LineRef(Line);
-
-    // We allow comments in headers. Technically this isn't part
-    // of the LSP specification, but makes writing tests easier.
-    if (LineRef.startswith("#"))
-      continue;
-
-    // Content-Length is a mandatory header, and the only one we handle.
-    if (LineRef.consume_front("Content-Length: ")) {
-      if (ContentLength != 0) {
-        elog("Warning: Duplicate Content-Length header received. "
-             "The previous value for this message ({0}) was ignored.",
-             ContentLength);
-      }
-      llvm::getAsUnsignedInteger(LineRef.trim(), 0, ContentLength);
-      continue;
-    } else if (!LineRef.trim().empty()) {
-      // It's another header, ignore it.
-      continue;
-    } else {
-      // An empty line indicates the end of headers.
-      // Go ahead and read the JSON.
-      break;
-    }
-  }
-
-  // The fuzzer likes crashing us by sending "Content-Length: 9999999999999999"
-  if (ContentLength > 1 << 30) { // 1024M
-    elog("Refusing to read message with long Content-Length: {0}. "
-         "Expect protocol errors",
-         ContentLength);
-    return llvm::None;
-  }
-  if (ContentLength == 0) {
-    log("Warning: Missing Content-Length header, or zero-length message.");
-    return llvm::None;
-  }
-
-  std::string JSON(ContentLength, '\0');
-  for (size_t Pos = 0, Read; Pos < ContentLength; Pos += Read) {
-    // Handle EINTR which is sent when a debugger attaches on some platforms.
-    Read = llvm::sys::RetryAfterSignal(0u, ::fread, &JSON[Pos], 1,
-                                       ContentLength - Pos, In);
-    Out.mirrorInput(StringRef(&JSON[Pos], Read));
-    if (Read == 0) {
-      elog("Input was aborted. Read only {0} bytes of expected {1}.", Pos,
-           ContentLength);
-      return llvm::None;
-    }
-    clearerr(In); // If we're done, the error was transient. If we're not done,
-                  // either it was transient or we'll see it again on retry.
-    Pos += Read;
-  }
-  return std::move(JSON);
-}
-
-// For lit tests we support a simplified syntax:
-// - messages are delimited by '---' on a line by itself
-// - lines starting with # are ignored.
-// This is a testing path, so favor simplicity over performance here.
-// When returning None, feof() or ferror() will be set.
-static llvm::Optional<std::string> readDelimitedMessage(std::FILE *In,
-                                                        JSONOutput &Out) {
-  std::string JSON;
-  std::string Line;
-  while (readLine(In, Line)) {
-    auto LineRef = llvm::StringRef(Line).trim();
-    if (LineRef.startswith("#")) // comment
-      continue;
-
-    // found a delimiter
-    if (LineRef.rtrim() == "---")
-      break;
-
-    JSON += Line;
-  }
-
-  if (ferror(In)) {
-    elog("Input error while reading message!");
-    return llvm::None;
-  } else { // Including EOF
-    Out.mirrorInput(
-        llvm::formatv("Content-Length: {0}\r\n\r\n{1}", JSON.size(), JSON));
-    return std::move(JSON);
-  }
-}
-
 // The use of C-style std::FILE* IO deserves some explanation.
 // Previously, std::istream was used. When a debugger attached on MacOS, the
 // process received EINTR, the stream went bad, and clangd exited.
 // A retry-on-EINTR loop around reads solved this problem, but caused clangd to
 // 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,
-                                   JSONStreamStyle InputStyle,
-                                   JSONRPCDispatcher &Dispatcher,
-                                   bool &IsDone) {
-  auto &ReadMessage =
-      (InputStyle == Delimited) ? readDelimitedMessage : readStandardMessage;
-  while (!IsDone && !feof(In)) {
-    if (ferror(In)) {
-      elog("IO error: {0}", llvm::sys::StrError());
-      return;
-    }
-    if (auto JSON = ReadMessage(In, Out)) {
-      if (auto Doc = json::parse(*JSON)) {
-        // Log the formatted message.
-        vlog(Out.Pretty ? "<<< {0:2}\n" : "<<< {0}\n", *Doc);
-        // Finally, execute the action for this JSON message.
-        if (!Dispatcher.call(*Doc, Out))
-          elog("JSON dispatch failed!");
-      } else {
-        // Parse error. Log the raw message.
-        vlog("<<< {0}\n", *JSON);
-        elog("JSON parse error: {0}", llvm::toString(Doc.takeError()));
-      }
-    }
-  }
+llvm::Error JSONRPCDispatcher::runLanguageServerLoop(Transport &Transport) {
+  // Propagate transport to all handlers so they can reply.
+  WithContextValue WithTransport(CurrentTransport, &Transport);
+  return Transport.loop(*this);
 }
 
 const json::Value *clangd::getRequestId() {
Index: clangd/ClangdLSPServer.h
===================================================================
--- clangd/ClangdLSPServer.h
+++ clangd/ClangdLSPServer.h
@@ -37,7 +37,8 @@
   /// 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(Transport &Transport,
+                  const clangd::CodeCompleteOptions &CCOpts,
                   llvm::Optional<Path> CompileCommandsDir,
                   bool ShouldUseInMemoryCDB, const ClangdServer::Options &Opts);
 
@@ -47,8 +48,7 @@
   /// 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 run();
 
 private:
   // Implement DiagnosticsConsumer.
@@ -89,16 +89,10 @@
   void reparseOpenedFiles();
   void applyConfiguration(const ClangdConfigurationParamsChange &Settings);
 
-  JSONOutput &Out;
   /// Used to indicate that the 'shutdown' request was received from the
   /// Language Server client.
   bool ShutdownRequestReceived = false;
 
-  /// Used to indicate that the 'exit' notification was received from the
-  /// Language Server client.
-  /// It's used to break out of the LSP parsing loop.
-  bool IsDone = false;
-
   std::mutex FixItsMutex;
   typedef std::map<clangd::Diagnostic, std::vector<Fix>, LSPDiagnosticCompare>
       DiagnosticToReplacementMap;
@@ -153,6 +147,7 @@
     bool IsDirectoryBased;
   };
 
+  clangd::Transport &Transport;
   // Various ClangdServer parameters go here. It's important they're created
   // before ClangdServer.
   CompilationDB CDB;
Index: clangd/ClangdLSPServer.cpp
===================================================================
--- clangd/ClangdLSPServer.cpp
+++ clangd/ClangdLSPServer.cpp
@@ -152,7 +152,9 @@
   reply(nullptr);
 }
 
-void ClangdLSPServer::onExit(ExitParams &Params) { IsDone = true; }
+void ClangdLSPServer::onExit(ExitParams &Params) {
+  // XXX handler should return true.
+}
 
 void ClangdLSPServer::onDocumentDidOpen(DidOpenTextDocumentParams &Params) {
   PathRef File = Params.textDocument.uri.file();
@@ -470,39 +472,41 @@
                          });
 }
 
-ClangdLSPServer::ClangdLSPServer(JSONOutput &Out,
+ClangdLSPServer::ClangdLSPServer(class Transport &Transport,
                                  const clangd::CodeCompleteOptions &CCOpts,
                                  llvm::Optional<Path> CompileCommandsDir,
                                  bool ShouldUseInMemoryCDB,
                                  const ClangdServer::Options &Opts)
-    : Out(Out), CDB(ShouldUseInMemoryCDB ? CompilationDB::makeInMemory()
-                                         : CompilationDB::makeDirectoryBased(
-                                               std::move(CompileCommandsDir))),
+    : Transport(Transport),
+      CDB(ShouldUseInMemoryCDB ? CompilationDB::makeInMemory()
+                               : CompilationDB::makeDirectoryBased(
+                                     std::move(CompileCommandsDir))),
       CCOpts(CCOpts), SupportedSymbolKinds(defaultSymbolKinds()),
       SupportedCompletionItemKinds(defaultCompletionItemKinds()),
       Server(new ClangdServer(CDB.getCDB(), FSProvider, /*DiagConsumer=*/*this,
                               Opts)) {}
 
-bool ClangdLSPServer::run(std::FILE *In, JSONStreamStyle InputStyle) {
-  assert(!IsDone && "Run was called before");
+bool ClangdLSPServer::run() {
   assert(Server);
 
   // Set up JSONRPCDispatcher.
   JSONRPCDispatcher Dispatcher([](const json::Value &Params) {
     replyError(ErrorCode::MethodNotFound, "method not found");
+    return false;
   });
   registerCallbackHandlers(Dispatcher, /*Callbacks=*/*this);
 
   // Run the Language Server loop.
-  runLanguageServerLoop(In, Out, InputStyle, Dispatcher, IsDone);
+  bool CleanExit = true;
+  if (auto Err = Dispatcher.runLanguageServerLoop(Transport)) {
+    elog("Transport error: {0}", std::move(Err));
+    CleanExit = false;
+  }
 
-  // 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;
   // Destroy ClangdServer to ensure all worker threads finish.
   Server.reset();
 
-  return ShutdownRequestReceived;
+  return CleanExit && ShutdownRequestReceived;
 }
 
 std::vector<Fix> ClangdLSPServer::getFixes(StringRef File,
@@ -562,15 +566,11 @@
   }
 
   // Publish diagnostics.
-  Out.writeMessage(json::Object{
-      {"jsonrpc", "2.0"},
-      {"method", "textDocument/publishDiagnostics"},
-      {"params",
-       json::Object{
-           {"uri", URIForFile{File}},
-           {"diagnostics", std::move(DiagnosticsJSON)},
-       }},
-  });
+  Transport.notify("textDocument/publishDiagnostics",
+                   json::Object{
+                       {"uri", URIForFile{File}},
+                       {"diagnostics", std::move(DiagnosticsJSON)},
+                   });
 }
 
 void ClangdLSPServer::reparseOpenedFiles() {
Index: clangd/CMakeLists.txt
===================================================================
--- clangd/CMakeLists.txt
+++ clangd/CMakeLists.txt
@@ -26,6 +26,7 @@
   GlobalCompilationDatabase.cpp
   Headers.cpp
   JSONRPCDispatcher.cpp
+  JSONTransport.cpp
   Logger.cpp
   Protocol.cpp
   ProtocolHandlers.cpp
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to