diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -666,8 +666,9 @@ log("Trying to incrementally change non-added document: {0}", File); return; } + std::string NewCode(*Code); for (const auto &Change : Params.contentChanges) { - if (auto Err = applyChange(*Code, Change)) { + if (auto Err = applyChange(NewCode, Change)) { // If this fails, we are most likely going to be not in sync anymore with // the client. It is better to remove the draft and let further // operations fail rather than giving wrong results. @@ -676,7 +677,7 @@ return; } } - Server->addDocument(File, *Code, encodeVersion(Params.textDocument.version), + Server->addDocument(File, NewCode, encodeVersion(Params.textDocument.version), WantDiags, Params.forceRebuild); } diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -348,7 +348,9 @@ /// FIXME: those metrics might be useful too, we should add them. llvm::StringMap fileStats() const; - llvm::Optional getDraft(PathRef File) const; + /// Gets the contents of a currently tracked file. Returns nullptr if the file + /// isn't being tracked. + std::shared_ptr getDraft(PathRef File) const; // Blocks the main thread until the server is idle. Only for use in tests. // Returns false if the timeout expires. @@ -394,6 +396,8 @@ // Only written from the main thread (despite being threadsafe). // FIXME: TUScheduler also keeps these, unify? DraftStore DraftMgr; + + std::unique_ptr DirtyFS; }; } // namespace clangd diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -106,6 +106,23 @@ ClangdServer::Callbacks *ServerCallbacks; }; +class DraftStoreFS : public ThreadsafeFS { +public: + DraftStoreFS(const ThreadsafeFS &Base, const DraftStore &Drafts) + : Base(Base), DirtyFiles(Drafts) {} + +private: + llvm::IntrusiveRefCntPtr viewImpl() const override { + auto OFS = llvm::makeIntrusiveRefCnt( + Base.view(llvm::None)); + OFS->pushOverlay(DirtyFiles.asVFS()); + return OFS; + } + + const ThreadsafeFS &Base; + const DraftStore &DirtyFiles; +}; + } // namespace ClangdServer::Options ClangdServer::optsForTest() { @@ -132,7 +149,8 @@ : FeatureModules(Opts.FeatureModules), CDB(CDB), TFS(TFS), DynamicIdx(Opts.BuildDynamicSymbolIndex ? new FileIndex() : nullptr), ClangTidyProvider(Opts.ClangTidyProvider), - WorkspaceRoot(Opts.WorkspaceRoot) { + WorkspaceRoot(Opts.WorkspaceRoot), + DirtyFS(std::make_unique(TFS, DraftMgr)) { // Pass a callback into `WorkScheduler` to extract symbols from a newly // parsed file and rebuild the file index synchronously each time an AST // is parsed. @@ -220,14 +238,14 @@ for (const Path &FilePath : DraftMgr.getActiveFiles()) if (Filter(FilePath)) if (auto Draft = DraftMgr.getDraft(FilePath)) // else disappeared in race? - addDocument(FilePath, std::move(Draft->Contents), Draft->Version, + addDocument(FilePath, *Draft->Contents, Draft->Version, WantDiagnostics::Auto); } -llvm::Optional ClangdServer::getDraft(PathRef File) const { +std::shared_ptr ClangdServer::getDraft(PathRef File) const { auto Draft = DraftMgr.getDraft(File); if (!Draft) - return llvm::None; + return nullptr; return std::move(Draft->Contents); } diff --git a/clang-tools-extra/clangd/DraftStore.h b/clang-tools-extra/clangd/DraftStore.h --- a/clang-tools-extra/clangd/DraftStore.h +++ b/clang-tools-extra/clangd/DraftStore.h @@ -13,6 +13,7 @@ #include "support/Path.h" #include "clang/Basic/LLVM.h" #include "llvm/ADT/StringMap.h" +#include "llvm/Support/VirtualFileSystem.h" #include #include #include @@ -27,7 +28,7 @@ class DraftStore { public: struct Draft { - std::string Contents; + std::shared_ptr Contents; std::string Version; }; @@ -47,9 +48,15 @@ /// Remove the draft from the store. void removeDraft(PathRef File); + llvm::IntrusiveRefCntPtr asVFS() const; + private: + struct DraftAndTime { + Draft Draft; + std::time_t MTime; + }; mutable std::mutex Mutex; - llvm::StringMap Drafts; + llvm::StringMap Drafts; }; } // namespace clangd diff --git a/clang-tools-extra/clangd/DraftStore.cpp b/clang-tools-extra/clangd/DraftStore.cpp --- a/clang-tools-extra/clangd/DraftStore.cpp +++ b/clang-tools-extra/clangd/DraftStore.cpp @@ -11,6 +11,8 @@ #include "support/Logger.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/Errc.h" +#include "llvm/Support/VirtualFileSystem.h" +#include namespace clang { namespace clangd { @@ -22,7 +24,7 @@ if (It == Drafts.end()) return None; - return It->second; + return It->second.Draft; } std::vector DraftStore::getActiveFiles() const { @@ -75,10 +77,11 @@ llvm::StringRef Contents) { std::lock_guard Lock(Mutex); - Draft &D = Drafts[File]; - updateVersion(D, Version); - D.Contents = Contents.str(); - return D.Version; + auto &D = Drafts[File]; + updateVersion(D.Draft, Version); + std::time(&D.MTime); + D.Draft.Contents = std::make_shared(Contents); + return D.Draft.Version; } void DraftStore::removeDraft(PathRef File) { @@ -87,5 +90,39 @@ Drafts.erase(File); } +namespace { + +/// A read only MemoryBuffer shares ownership of a ref counted string. The +/// shared string object must not be modified while an owned by this buffer. +class SharedStringBuffer : public llvm::MemoryBuffer { + const std::shared_ptr BufferContents; + const std::string Name; + +public: + BufferKind getBufferKind() const override { + return MemoryBuffer::MemoryBuffer_Malloc; + } + + StringRef getBufferIdentifier() const override { return Name; } + + SharedStringBuffer(std::shared_ptr Data, StringRef Name) + : BufferContents(std::move(Data)), Name(Name) { + assert(BufferContents && "Can't create from empty shared_ptr"); + MemoryBuffer::init(BufferContents->c_str(), + BufferContents->c_str() + BufferContents->size(), + /*RequiresNullTerminator=*/true); + } +}; +} // namespace + +llvm::IntrusiveRefCntPtr DraftStore::asVFS() const { + auto MemFS = llvm::makeIntrusiveRefCnt(); + std::lock_guard Guard(Mutex); + for (const auto &Draft : Drafts) + MemFS->addFile(Draft.getKey(), Draft.getValue().MTime, + std::make_unique( + Draft.getValue().Draft.Contents, Draft.getKey())); + return MemFS; +} } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/DraftStoreTests.cpp b/clang-tools-extra/clangd/unittests/DraftStoreTests.cpp --- a/clang-tools-extra/clangd/unittests/DraftStoreTests.cpp +++ b/clang-tools-extra/clangd/unittests/DraftStoreTests.cpp @@ -24,20 +24,20 @@ EXPECT_EQ("25", DS.addDraft(File, "25", "")); EXPECT_EQ("25", DS.getDraft(File)->Version); - EXPECT_EQ("", DS.getDraft(File)->Contents); + EXPECT_EQ("", *DS.getDraft(File)->Contents); EXPECT_EQ("26", DS.addDraft(File, "", "x")); EXPECT_EQ("26", DS.getDraft(File)->Version); - EXPECT_EQ("x", DS.getDraft(File)->Contents); + EXPECT_EQ("x", *DS.getDraft(File)->Contents); EXPECT_EQ("27", DS.addDraft(File, "", "x")) << "no-op change"; EXPECT_EQ("27", DS.getDraft(File)->Version); - EXPECT_EQ("x", DS.getDraft(File)->Contents); + EXPECT_EQ("x", *DS.getDraft(File)->Contents); // We allow versions to go backwards. EXPECT_EQ("7", DS.addDraft(File, "7", "y")); EXPECT_EQ("7", DS.getDraft(File)->Version); - EXPECT_EQ("y", DS.getDraft(File)->Contents); + EXPECT_EQ("y", *DS.getDraft(File)->Contents); } } // namespace