Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -117,7 +117,7 @@ void call(StringRef Method, llvm::json::Value Params); void notify(StringRef Method, llvm::json::Value Params); - RealFileSystemProvider FSProvider; + DraftFileSystemProvider FSProvider; /// Options used for code completion clangd::CodeCompleteOptions CCOpts; /// Options used for diagnostics. Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -454,15 +454,15 @@ void ClangdLSPServer::onRename(const RenameParams &Params, Callback Reply) { Path File = Params.textDocument.uri.file(); - Optional Code = DraftMgr.getDraft(File); - if (!Code) + auto Draft = DraftMgr.getDraft(File); + if (!Draft) return Reply(make_error("onRename called for non-added file", ErrorCode::InvalidParams)); Server->rename( File, Params.position, Params.newName, Bind( - [File, Code, + [File, Draft, Params](decltype(Reply) Reply, Expected> Replacements) { if (!Replacements) @@ -472,7 +472,7 @@ // Server Protocol. Fuse them into one big JSON array. std::vector Edits; for (const auto &R : *Replacements) - Edits.push_back(replacementToEdit(*Code, R)); + Edits.push_back(replacementToEdit(Draft->Content, R)); WorkspaceEdit WE; WE.changes = {{Params.textDocument.uri.uri(), Edits}}; Reply(WE); @@ -491,15 +491,15 @@ const DocumentOnTypeFormattingParams &Params, Callback> Reply) { auto File = Params.textDocument.uri.file(); - auto Code = DraftMgr.getDraft(File); - if (!Code) + auto Draft = DraftMgr.getDraft(File); + if (!Draft) return Reply(make_error( "onDocumentOnTypeFormatting called for non-added file", ErrorCode::InvalidParams)); - auto ReplacementsOrError = Server->formatOnType(*Code, File, Params.position); + auto ReplacementsOrError = Server->formatOnType(Draft->Content, File, Params.position); if (ReplacementsOrError) - Reply(replacementsToEdits(*Code, ReplacementsOrError.get())); + Reply(replacementsToEdits(Draft->Content, ReplacementsOrError.get())); else Reply(ReplacementsOrError.takeError()); } @@ -508,15 +508,15 @@ const DocumentRangeFormattingParams &Params, Callback> Reply) { auto File = Params.textDocument.uri.file(); - auto Code = DraftMgr.getDraft(File); - if (!Code) + auto Draft = DraftMgr.getDraft(File); + if (!Draft) return Reply(make_error( "onDocumentRangeFormatting called for non-added file", ErrorCode::InvalidParams)); - auto ReplacementsOrError = Server->formatRange(*Code, File, Params.range); + auto ReplacementsOrError = Server->formatRange(Draft->Content, File, Params.range); if (ReplacementsOrError) - Reply(replacementsToEdits(*Code, ReplacementsOrError.get())); + Reply(replacementsToEdits(Draft->Content, ReplacementsOrError.get())); else Reply(ReplacementsOrError.takeError()); } @@ -525,15 +525,15 @@ const DocumentFormattingParams &Params, Callback> Reply) { auto File = Params.textDocument.uri.file(); - auto Code = DraftMgr.getDraft(File); - if (!Code) + auto Draft = DraftMgr.getDraft(File); + if (!Draft) return Reply( make_error("onDocumentFormatting called for non-added file", ErrorCode::InvalidParams)); - auto ReplacementsOrError = Server->formatFile(*Code, File); + auto ReplacementsOrError = Server->formatFile(Draft->Content, File); if (ReplacementsOrError) - Reply(replacementsToEdits(*Code, ReplacementsOrError.get())); + Reply(replacementsToEdits(Draft->Content, ReplacementsOrError.get())); else Reply(ReplacementsOrError.takeError()); } @@ -690,7 +690,8 @@ Optional CompileCommandsDir, bool UseDirBasedCDB, const ClangdServer::Options &Opts) - : Transp(Transp), MsgHandler(new MessageHandler(*this)), CCOpts(CCOpts), + : Transp(Transp), MsgHandler(new MessageHandler(*this)), + FSProvider(DraftMgr), CCOpts(CCOpts), SupportedSymbolKinds(defaultSymbolKinds()), SupportedCompletionItemKinds(defaultCompletionItemKinds()), UseDirBasedCDB(UseDirBasedCDB), @@ -783,7 +784,7 @@ void ClangdLSPServer::reparseOpenedFiles() { for (const Path &FilePath : DraftMgr.getActiveFiles()) - Server->addDocument(FilePath, *DraftMgr.getDraft(FilePath), + Server->addDocument(FilePath, DraftMgr.getDraft(FilePath)->Content, WantDiagnostics::Auto); } Index: clangd/DraftStore.h =================================================================== --- clangd/DraftStore.h +++ clangd/DraftStore.h @@ -14,6 +14,7 @@ #include "Protocol.h" #include "clang/Basic/LLVM.h" #include "llvm/ADT/StringMap.h" +#include "llvm/Support/Chrono.h" #include #include #include @@ -21,6 +22,11 @@ namespace clang { namespace clangd { +struct Draft { + std::string Content; + llvm::sys::TimePoint<> ModifyTime; +}; + /// A thread-safe container for files opened in a workspace, addressed by /// filenames. The contents are owned by the DraftStore. This class supports /// both whole and incremental updates of the documents. @@ -28,7 +34,7 @@ public: /// \return Contents of the stored document. /// For untracked files, a llvm::None is returned. - llvm::Optional getDraft(PathRef File) const; + llvm::Optional getDraft(PathRef File) const; /// \return List of names of the drafts in this store. std::vector getActiveFiles() const; @@ -51,7 +57,7 @@ private: mutable std::mutex Mutex; - llvm::StringMap Drafts; + llvm::StringMap Drafts; }; } // namespace clangd Index: clangd/DraftStore.cpp =================================================================== --- clangd/DraftStore.cpp +++ clangd/DraftStore.cpp @@ -15,7 +15,7 @@ namespace clang { namespace clangd { -Optional DraftStore::getDraft(PathRef File) const { +Optional DraftStore::getDraft(PathRef File) const { std::lock_guard Lock(Mutex); auto It = Drafts.find(File); @@ -38,7 +38,7 @@ void DraftStore::addDraft(PathRef File, StringRef Contents) { std::lock_guard Lock(Mutex); - Drafts[File] = Contents; + Drafts[File] = { Contents, llvm::sys::TimePoint<>::clock::now() }; } Expected @@ -53,7 +53,7 @@ llvm::errc::invalid_argument); } - std::string Contents = EntryIt->second; + std::string Contents = EntryIt->second.Content; for (const TextDocumentContentChangeEvent &Change : Changes) { if (!Change.range) { @@ -105,7 +105,7 @@ Contents = std::move(NewContents); } - EntryIt->second = Contents; + Drafts[File] = { Contents, llvm::sys::TimePoint<>::clock::now() }; return Contents; } Index: clangd/FS.h =================================================================== --- clangd/FS.h +++ clangd/FS.h @@ -10,6 +10,8 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FS_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FS_H +#include "DraftStore.h" +#include "Logger.h" #include "clang/Basic/LLVM.h" #include "llvm/ADT/Optional.h" #include "llvm/Support/VirtualFileSystem.h" @@ -17,6 +19,75 @@ namespace clang { namespace clangd { +class DraftFile : public llvm::vfs::File { + std::unique_ptr F; + Draft DraftInfo; + +public: + DraftFile(std::unique_ptr F, Draft D) + : F(std::move(F)), DraftInfo(std::move(D)) {} + + llvm::ErrorOr getName() override { return F->getName(); } + + llvm::ErrorOr> + getBuffer(const Twine &Name, int64_t FileSize, bool RequiresNullTerminator, + bool IsVolatile) override { + (void)IsVolatile; + (void)RequiresNullTerminator; + + if (FileSize < 0) { + FileSize = DraftInfo.Content.size(); + } + + return llvm::MemoryBuffer::getMemBufferCopy( + llvm::StringRef(DraftInfo.Content).substr(0, FileSize), Name); + } + + llvm::ErrorOr status() override { + auto RealStatus = F->status(); + + if (!RealStatus) { + return RealStatus; + } + + llvm::vfs::Status Result( + RealStatus->getName(), RealStatus->getUniqueID(), DraftInfo.ModifyTime, + RealStatus->getUser(), RealStatus->getGroup(), DraftInfo.Content.size(), + RealStatus->getType(), RealStatus->getPermissions()); + + Result.IsVFSMapped = RealStatus->IsVFSMapped; + return Result; + } + + std::error_code close() override { return F->close(); } +}; + +/// Wrapper around real FS. If file was changed in the DraftStrore, returns +/// changed content, instead of real +class DraftFileSystem : public llvm::vfs::ProxyFileSystem { + DraftStore &DS; + +public: + DraftFileSystem(IntrusiveRefCntPtr FS, DraftStore &DS) + : llvm::vfs::ProxyFileSystem(FS), DS(DS) {} + + llvm::ErrorOr> + openFileForRead(const Twine &Path) override { + auto Result = getUnderlyingFS().openFileForRead(Path); + + if (!Result) { + return Result; + } + + if (auto D = DS.getDraft(Path.str())) { + return std::unique_ptr( + new DraftFile(std::move(*Result), std::move(*D))); + } + + return Result; + } +}; + /// Records status information for files open()ed or stat()ed during preamble /// build (except for the main file), so we can avoid stat()s on the underlying /// FS when reusing the preamble. For example, code completion can re-stat files Index: clangd/FSProvider.h =================================================================== --- clangd/FSProvider.h +++ clangd/FSProvider.h @@ -10,6 +10,7 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FSPROVIDER_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FSPROVIDER_H +#include "FS.h" #include "llvm/ADT/IntrusiveRefCntPtr.h" #include "llvm/Support/VirtualFileSystem.h" @@ -38,6 +39,20 @@ } }; +class DraftFileSystemProvider : public FileSystemProvider { + DraftStore &DS; + +public: + DraftFileSystemProvider(DraftStore &DS) : DS(DS) {} + + llvm::IntrusiveRefCntPtr + getFileSystem() const override { + static llvm::IntrusiveRefCntPtr Result( + new DraftFileSystem(llvm::vfs::getRealFileSystem(), DS)); + return Result; + } +}; + } // namespace clangd } // namespace clang Index: unittests/clangd/DraftStoreTests.cpp =================================================================== --- unittests/clangd/DraftStoreTests.cpp +++ unittests/clangd/DraftStoreTests.cpp @@ -53,7 +53,7 @@ Expected Result = DS.updateDraft(Path, {Event}); ASSERT_TRUE(!!Result); EXPECT_EQ(*Result, SrcAfter.code()); - EXPECT_EQ(*DS.getDraft(Path), SrcAfter.code()); + EXPECT_EQ(DS.getDraft(Path)->Content, SrcAfter.code()); } } @@ -83,7 +83,7 @@ ASSERT_TRUE(!!Result) << toString(Result.takeError()); EXPECT_EQ(*Result, FinalSrc.code()); - EXPECT_EQ(*DS.getDraft(Path), FinalSrc.code()); + EXPECT_EQ(DS.getDraft(Path)->Content, FinalSrc.code()); } TEST(DraftStoreIncrementalUpdateTest, Simple) { @@ -339,9 +339,9 @@ EXPECT_EQ(toString(Result.takeError()), "UTF-16 offset 100 is invalid for line 0"); - Optional Contents = DS.getDraft(File); - EXPECT_TRUE(Contents); - EXPECT_EQ(*Contents, OriginalContents); + Optional NewDraft = DS.getDraft(File); + EXPECT_TRUE(NewDraft); + EXPECT_EQ(NewDraft->Content, OriginalContents); } } // namespace