Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -98,7 +98,7 @@ reply(json::obj{ {{"capabilities", json::obj{ - {"textDocumentSync", 1}, + {"textDocumentSync", 2}, {"documentFormattingProvider", true}, {"documentRangeFormattingProvider", true}, {"documentOnTypeFormattingProvider", @@ -153,9 +153,10 @@ if (Params.wantDiagnostics.hasValue()) WantDiags = Params.wantDiagnostics.getValue() ? WantDiagnostics::Yes : WantDiagnostics::No; - // We only support full syncing right now. - Server.addDocument(Params.textDocument.uri.file(), - Params.contentChanges[0].text, WantDiags); + + const TextDocumentContentChangeEvent &Change = Params.contentChanges[0]; + Server.addDocument(Params.textDocument.uri.file(), Change.text, WantDiags, + Change.range); } void ClangdLSPServer::onFileEvent(DidChangeWatchedFilesParams &Params) { Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -157,7 +157,8 @@ /// separate thread. When the parsing is complete, DiagConsumer passed in /// constructor will receive onDiagnosticsReady callback. void addDocument(PathRef File, StringRef Contents, - WantDiagnostics WD = WantDiagnostics::Auto); + WantDiagnostics WantDiags = WantDiagnostics::Auto, + llvm::Optional Range = {}); /// Remove \p File from list of tracked files, schedule a request to free /// resources associated with it. Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -117,10 +117,13 @@ } void ClangdServer::addDocument(PathRef File, StringRef Contents, - WantDiagnostics WantDiags) { - DocVersion Version = DraftMgr.updateDraft(File, Contents); + WantDiagnostics WantDiags, + llvm::Optional Range) { + std::string NewContents; + DocVersion Version = + DraftMgr.updateDraft(File, Contents, Range, &NewContents); auto TaggedFS = FSProvider.getTaggedFileSystem(File); - scheduleReparseAndDiags(File, VersionedDraft{Version, Contents.str()}, + scheduleReparseAndDiags(File, VersionedDraft{Version, std::move(NewContents)}, WantDiags, std::move(TaggedFS)); } Index: clangd/DraftStore.h =================================================================== --- clangd/DraftStore.h +++ clangd/DraftStore.h @@ -11,6 +11,7 @@ #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_DRAFTSTORE_H #include "Path.h" +#include "Protocol.h" #include "clang/Basic/LLVM.h" #include "llvm/ADT/StringMap.h" #include @@ -50,9 +51,14 @@ /// For untracked files, 0 is returned. DocVersion getVersion(PathRef File) const; - /// Replace contents of the draft for \p File with \p Contents. + /// Update the contents of the draft for \p File with \p Contents. If + /// \p Range is specified, \p Contents replaces the text in this range. + /// Otherwise, it represents the complete content of the document. + /// \p *NewContents is updated to contain the new complete content + /// of the document. /// \return The new version of the draft for \p File. - DocVersion updateDraft(PathRef File, StringRef Contents); + DocVersion updateDraft(PathRef File, StringRef Contents, + llvm::Optional Range, std::string *NewContents); /// Remove the contents of the draft /// \return The new version of the draft for \p File. DocVersion removeDraft(PathRef File); Index: clangd/DraftStore.cpp =================================================================== --- clangd/DraftStore.cpp +++ clangd/DraftStore.cpp @@ -8,6 +8,8 @@ //===----------------------------------------------------------------------===// #include "DraftStore.h" +#include "Logger.h" +#include "SourceCode.h" using namespace clang; using namespace clang::clangd; @@ -41,12 +43,38 @@ return It->second.Version; } -DocVersion DraftStore::updateDraft(PathRef File, StringRef Contents) { +DocVersion DraftStore::updateDraft(PathRef File, StringRef Contents, + llvm::Optional range, + std::string *NewContents) { std::lock_guard Lock(Mutex); - auto &Entry = Drafts[File]; + std::string newContents; + + if (range) { + // Can't specify a range if we don't have an existing draft. In that case, + // return the version unchanged. + if (!Entry.Draft.hasValue()) { + log(llvm::Twine( + "Trying to do incremental update on draft without content: ") + + File); + return Entry.Version; + } + + size_t startIndex = positionToOffset(*Entry.Draft, range->start); + size_t endIndex = positionToOffset(*Entry.Draft, range->end); + + newContents = Entry.Draft->substr(0, startIndex); + newContents += Contents; + newContents += Entry.Draft->substr(endIndex); + } else { + newContents = Contents; + } + DocVersion NewVersion = ++Entry.Version; - Entry.Draft = Contents; + Entry.Draft = newContents; + + *NewContents = std::move(newContents); + return NewVersion; } Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -284,7 +284,10 @@ bool fromJSON(const json::Expr &, DidCloseTextDocumentParams &); struct TextDocumentContentChangeEvent { - /// The new text of the document. + /// The range of the document that changed. + llvm::Optional range; + + /// The new text of the range/document. std::string text; }; bool fromJSON(const json::Expr &, TextDocumentContentChangeEvent &); Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -248,7 +248,7 @@ bool fromJSON(const json::Expr &Params, TextDocumentContentChangeEvent &R) { json::ObjectMapper O(Params); - return O && O.map("text", R.text); + return O && O.map("text", R.text) && O.map("range", R.range); } bool fromJSON(const json::Expr &Params, FormattingOptions &R) { Index: test/clangd/initialize-params-invalid.test =================================================================== --- test/clangd/initialize-params-invalid.test +++ test/clangd/initialize-params-invalid.test @@ -36,7 +36,7 @@ # CHECK-NEXT: "," # CHECK-NEXT: ] # CHECK-NEXT: }, -# CHECK-NEXT: "textDocumentSync": 1 +# CHECK-NEXT: "textDocumentSync": 2 # CHECK-NEXT: } # CHECK-NEXT: } --- Index: test/clangd/initialize-params.test =================================================================== --- test/clangd/initialize-params.test +++ test/clangd/initialize-params.test @@ -36,7 +36,7 @@ # CHECK-NEXT: "," # CHECK-NEXT: ] # CHECK-NEXT: }, -# CHECK-NEXT: "textDocumentSync": 1 +# CHECK-NEXT: "textDocumentSync": 2 # CHECK-NEXT: } # CHECK-NEXT: } --- Index: unittests/clangd/CMakeLists.txt =================================================================== --- unittests/clangd/CMakeLists.txt +++ unittests/clangd/CMakeLists.txt @@ -15,6 +15,7 @@ CodeCompleteTests.cpp CodeCompletionStringsTests.cpp ContextTests.cpp + DraftStoreTests.cpp FileIndexTests.cpp FuzzyMatchTests.cpp HeadersTests.cpp Index: unittests/clangd/DraftStoreTests.cpp =================================================================== --- /dev/null +++ unittests/clangd/DraftStoreTests.cpp @@ -0,0 +1,164 @@ +//===-- DraftStoreTests.cpp - Clangd unit tests -----------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "Annotations.h" +#include "DraftStore.h" +#include "Protocol.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +using namespace llvm; + +namespace { +using testing::ElementsAre; +using testing::IsEmpty; + +struct IncrementalTestStep { + StringRef Src; + StringRef Contents; +}; + +static void +runIncrementalUpdateTest(llvm::ArrayRef Steps) { + DraftStore DS; + std::string NewContents; + Annotations InitialSrc(Steps[0].Src); + const char Path[] = "/hello.cpp"; + + // Set the initial content. + DS.updateDraft(Path, InitialSrc.code(), {}, &NewContents); + EXPECT_EQ(NewContents, InitialSrc.code()); + EXPECT_EQ(*DS.getDraft(Path).Draft, InitialSrc.code()); + + // For each step, replace the range marked in \p Src with \p Contents, and + // verify that it's equal to the following step's \p Src. + for (size_t i = 1; i < Steps.size(); i++) { + Annotations SrcBefore(Steps[i - 1].Src); + Annotations SrcAfter(Steps[i].Src); + StringRef Contents = Steps[i - 1].Contents; + + DS.updateDraft(Path, Contents, SrcBefore.range(), &NewContents); + EXPECT_EQ(NewContents, SrcAfter.code()); + EXPECT_EQ(*DS.getDraft(Path).Draft, SrcAfter.code()); + } +} + +TEST(DraftStoreIncrementalUpdateTest, Simple) { + // clang-format off + IncrementalTestStep Steps[] = + { + // Replace a range + { +R"cpp(static int +hello[[World]]() +{})cpp", + "Universe" + }, + // Delete a range + { +R"cpp(static int +hello[[Universe]]() +{})cpp", + "" + }, + // Add a range + { +R"cpp(static int +hello[[]]() +{})cpp", + "Monde" + }, + { +R"cpp(static int +helloMonde() +{})cpp", + "" + } + }; + // clang-format on + + runIncrementalUpdateTest(Steps); +} + +TEST(DraftStoreIncrementalUpdateTest, MultiLine) { + // clang-format off + IncrementalTestStep Steps[] = + { + // Replace a range + { +R"cpp(static [[int +helloWorld]]() +{})cpp", +R"cpp(char +welcome)cpp" + }, + // Delete a range + { +R"cpp(static char[[ +welcome]]() +{})cpp", + "" + }, + // Add a range + { +R"cpp(static char[[]]() +{})cpp", + R"cpp( +cookies)cpp" + }, + { +R"cpp(static char +cookies() +{})cpp", + "" + } + }; + // clang-format on + + runIncrementalUpdateTest(Steps); +} + +TEST(DraftStoreUpdateTest, Simple) { + StringRef Src = + R"cpp(static int +helloWorld() +{})cpp"; + + DraftStore DS; + StringRef Path = "/hello.cpp"; + std::string NewContents; + + // Add a document + DS.updateDraft(Path, Src, {}, &NewContents); + EXPECT_EQ(NewContents, Src); + EXPECT_EQ(*DS.getDraft(Path).Draft, Src); + EXPECT_THAT(DS.getActiveFiles(), ElementsAre(Path)); + + StringRef Src2 = + R"cpp(static char +thisIsAFunction() +{})cpp"; + DS.updateDraft(Path, Src2, {}, &NewContents); + EXPECT_EQ(NewContents, Src2); + EXPECT_EQ(*DS.getDraft(Path).Draft, Src2); + EXPECT_THAT(DS.getActiveFiles(), ElementsAre(Path)); + + DS.removeDraft(Path); + EXPECT_FALSE(DS.getDraft(Path).Draft.hasValue()); + EXPECT_THAT(DS.getActiveFiles(), IsEmpty()); + // Remove a document + // Replace the whole content +} + +} // namespace +} // namespace clangd +} // namespace clang