bkramer created this revision.
Herald added a subscriber: mgorny.

This requires an accessible compilation database. The parsing is done
asynchronously on a separate thread.


https://reviews.llvm.org/D29886

Files:
  clangd/ASTManager.cpp
  clangd/ASTManager.h
  clangd/CMakeLists.txt
  clangd/ClangDMain.cpp
  clangd/DocumentStore.h
  test/clangd/diagnostics.test

Index: test/clangd/diagnostics.test
===================================================================
--- /dev/null
+++ test/clangd/diagnostics.test
@@ -0,0 +1,17 @@
+# RUN: clangd < %s | FileCheck %s
+# It is absolutely vital that this file has CRLF line endings.
+#
+Content-Length: 125
+
+{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}}
+#
+Content-Length: 152
+
+{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///foo.c","languageId":"c","version":1,"text":"void main() {}"}}}
+#
+# CHECK: {"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:///foo.c","diagnostics":[{"range":{"start": {"line": 0, "character": 1}, "end": {"line": 0, "character": 1}},"severity":2,"message":"return type of 'main' is not 'int'"},{"range":{"start": {"line": 0, "character": 1}, "end": {"line": 0, "character": 1}},"severity":3,"message":"change return type to 'int'"}]}}
+#
+#
+Content-Length: 44
+
+{"jsonrpc":"2.0","id":5,"method":"shutdown"}
Index: clangd/DocumentStore.h
===================================================================
--- clangd/DocumentStore.h
+++ clangd/DocumentStore.h
@@ -12,27 +12,62 @@
 
 #include "clang/Basic/LLVM.h"
 #include "llvm/ADT/StringMap.h"
+#include <mutex>
 #include <string>
 
 namespace clang {
 namespace clangd {
+class DocumentStore;
+
+struct DocumentStoreListener {
+  virtual ~DocumentStoreListener() = default;
+  virtual void onDocumentAdd(StringRef Uri, const DocumentStore &Docs) {}
+  virtual void onDocumentRemove(StringRef Uri, const DocumentStore &Docs) {}
+};
 
 /// A container for files opened in a workspace, addressed by URI. The contents
 /// are owned by the DocumentStore.
 class DocumentStore {
 public:
   /// Add a document to the store. Overwrites existing contents.
-  void addDocument(StringRef Uri, StringRef Text) { Docs[Uri] = Text; }
+  void addDocument(StringRef Uri, StringRef Text) {
+    {
+      std::lock_guard<std::mutex> Guard(DocsMutex);
+      Docs[Uri] = Text;
+    }
+    for (const auto &Listener : Listeners)
+      Listener->onDocumentAdd(Uri, *this);
+  }
   /// Delete a document from the store.
-  void removeDocument(StringRef Uri) { Docs.erase(Uri); }
+  void removeDocument(StringRef Uri) { Docs.erase(Uri);
+    for (const auto &Listener : Listeners)
+      Listener->onDocumentRemove(Uri, *this);
+  }
   /// Retrieve a document from the store. Empty string if it's unknown.
   StringRef getDocument(StringRef Uri) const {
+    // FIXME: This could be a reader lock.
+    std::lock_guard<std::mutex> Guard(DocsMutex);
     auto I = Docs.find(Uri);
     return I == Docs.end() ? StringRef("") : StringRef(I->second);
   }
 
+  void addListener(std::unique_ptr<DocumentStoreListener> DSL) {
+    Listeners.push_back(std::move(DSL));
+  }
+
+  std::vector<std::pair<StringRef, StringRef>> getAllDocuments() const {
+    std::vector<std::pair<StringRef, StringRef>> AllDocs;
+    std::lock_guard<std::mutex> Guard(DocsMutex);
+    for (const auto &P : Docs)
+      AllDocs.emplace_back(P.first(), P.second);
+    return AllDocs;
+  }
+
 private:
   llvm::StringMap<std::string> Docs;
+  std::vector<std::unique_ptr<DocumentStoreListener>> Listeners;
+
+  mutable std::mutex DocsMutex;
 };
 
 } // namespace clangd
Index: clangd/ClangDMain.cpp
===================================================================
--- clangd/ClangDMain.cpp
+++ clangd/ClangDMain.cpp
@@ -7,6 +7,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "ASTManager.h"
 #include "DocumentStore.h"
 #include "JSONRPCDispatcher.h"
 #include "ProtocolHandlers.h"
@@ -27,6 +28,8 @@
   // Set up a document store and intialize all the method handlers for JSONRPC
   // dispatching.
   DocumentStore Store;
+  auto *AST = new ASTManager(Out, Store);
+  Store.addListener(std::unique_ptr<ASTManager>(AST));
   JSONRPCDispatcher Dispatcher(llvm::make_unique<Handler>(Out));
   Dispatcher.registerHandler("initialize",
                              llvm::make_unique<InitializeHandler>(Out));
Index: clangd/CMakeLists.txt
===================================================================
--- clangd/CMakeLists.txt
+++ clangd/CMakeLists.txt
@@ -1,4 +1,5 @@
 add_clang_executable(clangd
+  ASTManager.cpp
   ClangDMain.cpp
   JSONRPCDispatcher.cpp
   Protocol.cpp
@@ -8,5 +9,7 @@
 target_link_libraries(clangd
   clangBasic
   clangFormat
+  clangFrontend
+  clangTooling
   LLVMSupport
   )
Index: clangd/ASTManager.h
===================================================================
--- /dev/null
+++ clangd/ASTManager.h
@@ -0,0 +1,73 @@
+//===--- ASTManager.h - Clang AST manager -----------------------*- 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_ASTMANAGER_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_ASTMANAGER_H
+
+#include "JSONRPCDispatcher.h"
+#include "DocumentStore.h"
+#include "llvm/ADT/IntrusiveRefCntPtr.h"
+#include <condition_variable>
+#include <thread>
+
+namespace clang {
+class ASTUnit;
+class DiagnosticsEngine;
+class PCHContainerOperations;
+namespace tooling {
+class CompilationDatabase;
+} // namespace tooling
+
+namespace clangd {
+
+class ASTManager : public DocumentStoreListener {
+public:
+  ASTManager(JSONOutput &Output, DocumentStore &Store);
+  ~ASTManager() override;
+
+  void onDocumentAdd(StringRef Uri, const DocumentStore &Docs) override;
+  // FIXME: Implement onDocumentRemove
+  // FIXME: Implement codeComplete
+
+private:
+  JSONOutput &Output;
+  DocumentStore &Store;
+
+  /// Loads a compilation database for URI. May return nullptr if it fails. The
+  /// database is cached for subsequent accesses.
+  clang::tooling::CompilationDatabase *
+  getOrCreateCompilationDatabaseForFile(StringRef Uri);
+  std::unique_ptr<clang::ASTUnit>
+  createASTUnitForFile(StringRef Uri, const DocumentStore &Docs);
+
+  void runWorker();
+
+  /// Clang objects.
+  llvm::StringMap<std::unique_ptr<clang::ASTUnit>> ASTs;
+  llvm::StringMap<std::unique_ptr<clang::tooling::CompilationDatabase>>
+      CompilationDatabases;
+  std::shared_ptr<clang::PCHContainerOperations> PCHs;
+
+  /// We run parsing on a separate thread. This thread looks into PendingRequest
+  /// as a 'one element work queue' as long as RequestIsPending is true.
+  std::thread ClangWorker;
+  std::string PendingRequest;
+  bool RequestIsPending = false;
+  /// Condition variable to wake up the worker thread.
+  std::condition_variable ClangRequestCV;
+  /// Lock for accesses to PendingRequest and RequestIsPending.
+  std::mutex RequestLock;
+  /// Setting Done to true will make the worker thread terminate.
+  std::atomic<bool> Done;
+};
+
+} // namespace clangd
+} // namespace clang
+
+#endif
Index: clangd/ASTManager.cpp
===================================================================
--- /dev/null
+++ clangd/ASTManager.cpp
@@ -0,0 +1,196 @@
+//===--- ASTManager.cpp - Clang AST manager -------------------------------===//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+
+#include "ASTManager.h"
+#include "JSONRPCDispatcher.h"
+#include "Protocol.h"
+#include "clang/Frontend/ASTUnit.h"
+#include "clang/Frontend/CompilerInstance.h"
+#include "clang/Tooling/CompilationDatabase.h"
+#include "llvm/Support/Format.h"
+#include "llvm/Support/Path.h"
+#include <mutex>
+#include <thread>
+using namespace clang;
+using namespace clangd;
+
+/// Retrieve a copy of the contents of every file in the store, for feeding into
+/// ASTUnit.
+static std::vector<ASTUnit::RemappedFile>
+getRemappedFiles(const DocumentStore &Docs) {
+  // FIXME: Use VFS instead. This would allow us to get rid of the chdir below.
+  std::vector<ASTUnit::RemappedFile> RemappedFiles;
+  for (const auto &P : Docs.getAllDocuments()) {
+    StringRef FileName = P.first;
+    FileName.consume_front("file://");
+    RemappedFiles.push_back(ASTUnit::RemappedFile(
+        FileName,
+        llvm::MemoryBuffer::getMemBufferCopy(P.second, FileName).release()));
+  }
+  return RemappedFiles;
+}
+
+/// Convert from clang diagnostic level to LSP severity.
+static int getSeverity(DiagnosticsEngine::Level L) {
+  switch (L) {
+  case DiagnosticsEngine::Remark:
+    return 4;
+  case DiagnosticsEngine::Note:
+    return 3;
+  case DiagnosticsEngine::Warning:
+    return 2;
+  case DiagnosticsEngine::Fatal:
+  case DiagnosticsEngine::Error:
+    return 1;
+  case DiagnosticsEngine::Ignored:
+    return 0;
+  }
+}
+
+ASTManager::ASTManager(JSONOutput &Output, DocumentStore &Store)
+    : Output(Output), Store(Store),
+      PCHs(std::make_shared<PCHContainerOperations>()),
+      ClangWorker([this]() { runWorker(); }) {}
+
+void ASTManager::runWorker() {
+  while (true) {
+    std::string File;
+
+    {
+      std::unique_lock<std::mutex> Lock(RequestLock);
+      // Check if there's another request pending. We keep parsing until
+      // our one-element queue is empty.
+      if (!RequestIsPending && !Done)
+        ClangRequestCV.wait(Lock);
+
+      if (!RequestIsPending && Done)
+        return;
+
+      RequestIsPending = false;
+      File = PendingRequest;
+    } // unlock.
+
+    auto &Unit = ASTs[File]; // Only one thread can access this at a time.
+
+    if (!Unit) {
+      Unit = createASTUnitForFile(File, this->Store);
+    } else {
+      // Do a reparse if this wasn't the first parse.
+      Unit->Reparse(PCHs, getRemappedFiles(this->Store));
+    }
+
+    if (!Unit)
+      continue;
+
+    // Send the diagnotics to the editor.
+    // FIXME: If the diagnostic comes from a different file, do we want to
+    // show them all? Right now we drop everything not coming from the
+    // main file.
+    // FIXME: Send FixIts to the editor.
+    std::string Diagnostics;
+    for (ASTUnit::stored_diag_iterator D = Unit->stored_diag_begin(),
+                                       DEnd = Unit->stored_diag_end();
+         D != DEnd; ++D) {
+      if (!D->getLocation().isValid() ||
+          !D->getLocation().getManager().isInMainFile(D->getLocation()))
+        continue;
+      Position P;
+      P.line = D->getLocation().getSpellingLineNumber() - 1;
+      P.character = D->getLocation().getSpellingColumnNumber();
+      Range R = {P, P};
+      Diagnostics +=
+          R"({"range":)" + Range::unparse(R) +
+          R"(,"severity":)" + std::to_string(getSeverity(D->getLevel())) +
+          R"(,"message":")" + llvm::yaml::escape(D->getMessage()) +
+          R"("},)";
+    }
+
+    if (!Diagnostics.empty())
+      Diagnostics.pop_back(); // Drop trailing comma.
+    this->Output.writeMessage(
+        R"({"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":")" +
+        File + R"(","diagnostics":[)" + Diagnostics + R"(]}})");
+  }
+}
+
+ASTManager::~ASTManager() {
+  {
+    std::lock_guard<std::mutex> Guard(RequestLock);
+    // Wake up the clang worker thread, then exit.
+    Done = true;
+    ClangRequestCV.notify_one();
+  }
+  ClangWorker.join();
+}
+
+void ASTManager::onDocumentAdd(StringRef Uri, const DocumentStore &Docs) {
+  std::lock_guard<std::mutex> Guard(RequestLock);
+  PendingRequest = Uri;
+  RequestIsPending = true;
+  ClangRequestCV.notify_one();
+}
+
+tooling::CompilationDatabase *
+ASTManager::getOrCreateCompilationDatabaseForFile(StringRef Uri) {
+  auto &I = CompilationDatabases[Uri];
+  if (I)
+    return I.get();
+
+  Uri.consume_front("file://");
+
+  std::string Error;
+  I = tooling::CompilationDatabase::autoDetectFromSource(Uri, Error);
+  return I.get();
+}
+
+std::unique_ptr<clang::ASTUnit>
+ASTManager::createASTUnitForFile(StringRef Uri, const DocumentStore &Docs) {
+  tooling::CompilationDatabase *CDB =
+      getOrCreateCompilationDatabaseForFile(Uri);
+
+  Uri.consume_front("file://");
+  std::vector<tooling::CompileCommand> Commands;
+
+  if (CDB) {
+    Commands = CDB->getCompileCommands(Uri);
+    // chdir. This is thread hostile.
+    if (!Commands.empty())
+      llvm::sys::fs::set_current_path(Commands.front().Directory);
+  }
+  if (Commands.empty()) {
+    // Add a fake command line if we know nothing.
+    Commands.push_back(tooling::CompileCommand(
+        llvm::sys::path::parent_path(Uri), llvm::sys::path::filename(Uri),
+        {"clang", "-fsyntax-only", Uri.str()}, ""));
+  }
+
+  // Inject the resource dir.
+  // FIXME: Don't overwrite it if it's already there.
+  static int Dummy; // Just an address in this process.
+  std::string ResourceDir =
+      CompilerInvocation::GetResourcesPath("clangd", (void *)&Dummy);
+  Commands.front().CommandLine.push_back("-resource-dir=" + ResourceDir);
+
+  IntrusiveRefCntPtr<DiagnosticsEngine> Diags =
+      CompilerInstance::createDiagnostics(new DiagnosticOptions);
+
+  std::vector<const char *> ArgStrs;
+  for (const auto &S : Commands.front().CommandLine)
+    ArgStrs.push_back(S.c_str());
+
+  return std::unique_ptr<clang::ASTUnit>(ASTUnit::LoadFromCommandLine(
+      &*ArgStrs.begin(), &*ArgStrs.end(), PCHs, Diags, ResourceDir,
+      /*OnlyLocalDecls=*/false, /*CaptureDiagnostics=*/true,
+      getRemappedFiles(Docs),
+      /*RemappedFilesKeepOriginalName=*/true,
+      /*PrecompilePreambleAfterNParses=*/1, /*TUKind=*/TU_Complete,
+      /*CacheCodeCompletionResults=*/true,
+      /*IncludeBriefCommentsInCodeCompletion=*/true,
+      /*AllowPCHWithCompilerErrors=*/true));
+}
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to