//===--- ClangdServer.cpp - Main clangd server code --------------*- C++-*-===// // // The LLVM Compiler Infrastructure // // This file is distributed under the University of Illinois Open Source // License. See LICENSE.TXT for details. // //===-------------------------------------------------------------------===// #include "ClangdServer.h" #include "clang/Format/Format.h" #include "clang/Frontend/ASTUnit.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" #include "clang/Tooling/CompilationDatabase.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/raw_ostream.h" #include using namespace clang; using namespace clang::clangd; namespace { std::vector formatCode(StringRef Code, StringRef Filename, ArrayRef Ranges) { // Call clang-format. // FIXME: Don't ignore style. format::FormatStyle Style = format::getLLVMStyle(); auto Result = format::reformat(Style, Code, Ranges, Filename); return std::vector(Result.begin(), Result.end()); } std::string getStandardResourceDir() { static int Dummy; // Just an address in this process. return CompilerInvocation::GetResourcesPath("clangd", (void *)&Dummy); } } // namespace size_t clangd::positionToOffset(StringRef Code, Position P) { size_t Offset = 0; for (int I = 0; I != P.line; ++I) { // FIXME: \r\n // FIXME: UTF-8 size_t F = Code.find('\n', Offset); if (F == StringRef::npos) return 0; // FIXME: Is this reasonable? Offset = F + 1; } return (Offset == 0 ? 0 : (Offset - 1)) + P.character; } /// Turn an offset in Code into a [line, column] pair. Position clangd::offsetToPosition(StringRef Code, size_t Offset) { StringRef JustBefore = Code.substr(0, Offset); // FIXME: \r\n // FIXME: UTF-8 int Lines = JustBefore.count('\n'); int Cols = JustBefore.size() - JustBefore.rfind('\n') - 1; return {Lines, Cols}; } Tagged> RealFileSystemProvider::getTaggedFileSystem(PathRef File) { return make_tagged(vfs::getRealFileSystem(), VFSTag()); } ClangdScheduler::ClangdScheduler(bool RunSynchronously) : RunSynchronously(RunSynchronously) { if (RunSynchronously) { // Don't start the worker thread if we're running synchronously return; } // Initialize Worker in ctor body, rather than init list to avoid potentially // using not-yet-initialized members Worker = std::thread([this]() { while (true) { std::function Request; // Pick request from the queue { std::unique_lock Lock(Mutex); // Wait for more requests. RequestCV.wait(Lock, [this] { return !RequestQueue.empty() || Done; }); if (Done) return; assert(!RequestQueue.empty() && "RequestQueue was empty"); // We process requests starting from the front of the queue. Users of // ClangdScheduler have a way to prioritise their requests by putting // them to the either side of the queue (using either addToEnd or // addToFront). Request = std::move(RequestQueue.front()); RequestQueue.pop_front(); } // unlock Mutex Request(); } }); } ClangdScheduler::~ClangdScheduler() { if (RunSynchronously) return; // no worker thread is running in that case { std::lock_guard Lock(Mutex); // Wake up the worker thread Done = true; } // unlock Mutex RequestCV.notify_one(); Worker.join(); } void ClangdScheduler::addToFront(std::function Request) { if (RunSynchronously) { Request(); return; } { std::lock_guard Lock(Mutex); RequestQueue.push_front(Request); } RequestCV.notify_one(); } void ClangdScheduler::addToEnd(std::function Request) { if (RunSynchronously) { Request(); return; } { std::lock_guard Lock(Mutex); RequestQueue.push_back(Request); } RequestCV.notify_one(); } ClangdServer::ClangdServer(GlobalCompilationDatabase &CDB, DiagnosticsConsumer &DiagConsumer, FileSystemProvider &FSProvider, bool RunSynchronously, llvm::Optional ResourceDir) : CDB(CDB), DiagConsumer(DiagConsumer), FSProvider(FSProvider), ResourceDir(ResourceDir ? ResourceDir->str() : getStandardResourceDir()), PCHs(std::make_shared()), WorkScheduler(RunSynchronously) {} void ClangdServer::addDocument(PathRef File, StringRef Contents) { DocVersion Version = DraftMgr.updateDraft(File, Contents); Path FileStr = File; WorkScheduler.addToFront([this, FileStr, Version]() { auto FileContents = DraftMgr.getDraft(FileStr); if (FileContents.Version != Version) return; // This request is outdated, do nothing assert(FileContents.Draft && "No contents inside a file that was scheduled for reparse"); auto TaggedFS = FSProvider.getTaggedFileSystem(FileStr); Units.runOnUnit( FileStr, *FileContents.Draft, ResourceDir, CDB, PCHs, TaggedFS.Value, [&](ClangdUnit const &Unit) { DiagConsumer.onDiagnosticsReady( FileStr, make_tagged(Unit.getLocalDiagnostics(), TaggedFS.Tag)); }); }); } void ClangdServer::removeDocument(PathRef File) { auto Version = DraftMgr.removeDraft(File); Path FileStr = File; WorkScheduler.addToFront([this, FileStr, Version]() { if (Version != DraftMgr.getVersion(FileStr)) return; // This request is outdated, do nothing Units.removeUnitIfPresent(FileStr); }); } void ClangdServer::forceReparse(PathRef File) { // The addDocument schedules the reparse even if the contents of the file // never changed, so we just call it here. addDocument(File, getDocument(File)); } Tagged> ClangdServer::codeComplete(PathRef File, Position Pos, llvm::Optional OverridenContents) { std::string DraftStorage; if (!OverridenContents) { auto FileContents = DraftMgr.getDraft(File); assert(FileContents.Draft && "codeComplete is called for non-added document"); DraftStorage = std::move(*FileContents.Draft); OverridenContents = DraftStorage; } std::vector Result; auto TaggedFS = FSProvider.getTaggedFileSystem(File); // It would be nice to use runOnUnitWithoutReparse here, but we can't // guarantee the correctness of code completion cache here if we don't do the // reparse. Units.runOnUnit(File, *OverridenContents, ResourceDir, CDB, PCHs, TaggedFS.Value, [&](ClangdUnit &Unit) { Result = Unit.codeComplete(*OverridenContents, Pos, TaggedFS.Value); }); return make_tagged(std::move(Result), TaggedFS.Tag); } std::vector ClangdServer::formatRange(PathRef File, Range Rng) { std::string Code = getDocument(File); size_t Begin = positionToOffset(Code, Rng.start); size_t Len = positionToOffset(Code, Rng.end) - Begin; return formatCode(Code, File, {tooling::Range(Begin, Len)}); } std::vector ClangdServer::formatFile(PathRef File) { // Format everything. std::string Code = getDocument(File); return formatCode(Code, File, {tooling::Range(0, Code.size())}); } std::vector ClangdServer::formatOnType(PathRef File, Position Pos) { // Look for the previous opening brace from the character position and // format starting from there. std::string Code = getDocument(File); size_t CursorPos = positionToOffset(Code, Pos); size_t PreviousLBracePos = StringRef(Code).find_last_of('{', CursorPos); if (PreviousLBracePos == StringRef::npos) PreviousLBracePos = CursorPos; size_t Len = 1 + CursorPos - PreviousLBracePos; return formatCode(Code, File, {tooling::Range(PreviousLBracePos, Len)}); } std::string ClangdServer::getDocument(PathRef File) { auto draft = DraftMgr.getDraft(File); assert(draft.Draft && "File is not tracked, cannot get contents"); return *draft.Draft; } std::string ClangdServer::dumpAST(PathRef File) { std::promise DumpPromise; auto DumpFuture = DumpPromise.get_future(); auto Version = DraftMgr.getVersion(File); WorkScheduler.addToEnd([this, &DumpPromise, File, Version]() { assert(DraftMgr.getVersion(File) == Version && "Version has changed"); Units.runOnExistingUnit(File, [&DumpPromise](ClangdUnit &Unit) { std::string Result; llvm::raw_string_ostream ResultOS(Result); Unit.dumpAST(ResultOS); ResultOS.flush(); DumpPromise.set_value(std::move(Result)); }); }); return DumpFuture.get(); } Tagged> ClangdServer::findDefinitions(PathRef File, Position Pos) { auto FileContents = DraftMgr.getDraft(File); assert(FileContents.Draft && "findDefinitions is called for non-added document"); std::vector Result; auto TaggedFS = FSProvider.getTaggedFileSystem(File); Units.runOnUnit(File, *FileContents.Draft, ResourceDir, CDB, PCHs, TaggedFS.Value, [&](ClangdUnit &Unit) { Result = Unit.findDefinitions(Pos); }); return make_tagged(std::move(Result), TaggedFS.Tag); }