// If the file is open in user's editor, make sure the version we
// saw and current version are compatible as this is the text that
// will be replaced by editors.
- if (!It.second.canApplyTo(*Draft)) {
+ if (!It.second.canApplyTo(Draft->Contents)) {
++InvalidFileCount;
LastInvalidFile = It.first();
}
const std::string &Contents = Params.textDocument.text;
- DraftMgr.addDraft(File, Contents);
+ DraftMgr.addDraft(File, Params.textDocument.version, Contents);
Server->addDocument(File, Contents, WantDiagnostics::Yes);
}
: WantDiagnostics::No;
PathRef File = Params.textDocument.uri.file();
- llvm::Expected<std::string> Contents =
- DraftMgr.updateDraft(File, Params.contentChanges);
- if (!Contents) {
+ llvm::Expected<DraftStore::Draft> Draft = DraftMgr.updateDraft(
+ File, Params.textDocument.version, Params.contentChanges);
+ if (!Draft) {
// 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.
DraftMgr.removeDraft(File);
Server->removeDocument(File);
- elog("Failed to update {0}: {1}", File, Contents.takeError());
+ elog("Failed to update {0}: {1}", File, Draft.takeError());
return;
}
- Server->addDocument(File, *Contents, WantDiags, Params.forceRebuild);
+ Server->addDocument(File, Draft->Contents, WantDiags, Params.forceRebuild);
}
void ClangdLSPServer::onFileEvent(const DidChangeWatchedFilesParams &Params) {
void ClangdLSPServer::onRename(const RenameParams &Params,
Callback<WorkspaceEdit> Reply) {
Path File = std::string(Params.textDocument.uri.file());
- llvm::Optional<std::string> Code = DraftMgr.getDraft(File);
- if (!Code)
+ if (!DraftMgr.getDraft(File))
return Reply(llvm::make_error<LSPError>(
"onRename called for non-added file", ErrorCode::InvalidParams));
Server->rename(
"onDocumentOnTypeFormatting called for non-added file",
ErrorCode::InvalidParams));
- Reply(Server->formatOnType(*Code, File, Params.position, Params.ch));
+ Reply(Server->formatOnType(Code->Contents, File, Params.position, Params.ch));
}
void ClangdLSPServer::onDocumentRangeFormatting(
"onDocumentRangeFormatting called for non-added file",
ErrorCode::InvalidParams));
- auto ReplacementsOrError = Server->formatRange(*Code, File, Params.range);
+ auto ReplacementsOrError =
+ Server->formatRange(Code->Contents, File, Params.range);
if (ReplacementsOrError)
- Reply(replacementsToEdits(*Code, ReplacementsOrError.get()));
+ Reply(replacementsToEdits(Code->Contents, ReplacementsOrError.get()));
else
Reply(ReplacementsOrError.takeError());
}
"onDocumentFormatting called for non-added file",
ErrorCode::InvalidParams));
- auto ReplacementsOrError = Server->formatFile(*Code, File);
+ auto ReplacementsOrError = Server->formatFile(Code->Contents, File);
if (ReplacementsOrError)
- Reply(replacementsToEdits(*Code, ReplacementsOrError.get()));
+ Reply(replacementsToEdits(Code->Contents, ReplacementsOrError.get()));
else
Reply(ReplacementsOrError.takeError());
}
// Running the lexer here would be more robust (e.g. we can detect comments
// and avoid triggering completion there), but we choose to err on the side
// of simplicity here.
- auto Offset = positionToOffset(*Code, Params.position,
+ auto Offset = positionToOffset(Code->Contents, Params.position,
/*AllowColumnsBeyondLineLength=*/false);
if (!Offset) {
vlog("could not convert position '{0}' to offset for file '{1}'",
return false;
if (Trigger == ">")
- return (*Code)[*Offset - 2] == '-'; // trigger only on '->'.
+ return Code->Contents[*Offset - 2] == '-'; // trigger only on '->'.
if (Trigger == ":")
- return (*Code)[*Offset - 2] == ':'; // trigger only on '::'.
+ return Code->Contents[*Offset - 2] == ':'; // trigger only on '::'.
assert(false && "unhandled trigger character");
return true;
}
// Reparse only opened files that were modified.
for (const Path &FilePath : DraftMgr.getActiveFiles())
if (ModifiedFiles.find(FilePath) != ModifiedFiles.end())
- Server->addDocument(FilePath, *DraftMgr.getDraft(FilePath),
+ Server->addDocument(FilePath, DraftMgr.getDraft(FilePath)->Contents,
WantDiagnostics::Auto);
}
//===----------------------------------------------------------------------===//
#include "DraftStore.h"
+#include "Logger.h"
#include "SourceCode.h"
#include "llvm/Support/Errc.h"
namespace clang {
namespace clangd {
-llvm::Optional<std::string> DraftStore::getDraft(PathRef File) const {
+llvm::Optional<DraftStore::Draft> DraftStore::getDraft(PathRef File) const {
std::lock_guard<std::mutex> Lock(Mutex);
auto It = Drafts.find(File);
return ResultVector;
}
-void DraftStore::addDraft(PathRef File, llvm::StringRef Contents) {
+static void updateVersion(DraftStore::Draft &D,
+ llvm::Optional<int64_t> Version) {
+ if (Version) {
+ // We treat versions as opaque, but the protocol says they increase.
+ if (*Version <= D.Version)
+ log("File version went from {0} to {1}", D.Version, Version);
+ D.Version = *Version;
+ } else {
+ // Note that if D was newly-created, this will bump D.Version from -1 to 0.
+ ++D.Version;
+ }
+}
+
+int64_t DraftStore::addDraft(PathRef File, llvm::Optional<int64_t> Version,
+ llvm::StringRef Contents) {
std::lock_guard<std::mutex> Lock(Mutex);
- Drafts[File] = std::string(Contents);
+ Draft &D = Drafts[File];
+ updateVersion(D, Version);
+ D.Contents = Contents.str();
+ return D.Version;
}
-llvm::Expected<std::string> DraftStore::updateDraft(
- PathRef File, llvm::ArrayRef<TextDocumentContentChangeEvent> Changes) {
+llvm::Expected<DraftStore::Draft> DraftStore::updateDraft(
+ PathRef File, llvm::Optional<int64_t> Version,
+ llvm::ArrayRef<TextDocumentContentChangeEvent> Changes) {
std::lock_guard<std::mutex> Lock(Mutex);
auto EntryIt = Drafts.find(File);
"Trying to do incremental update on non-added document: " + File,
llvm::errc::invalid_argument);
}
-
- std::string Contents = EntryIt->second;
+ Draft &D = EntryIt->second;
+ std::string Contents = EntryIt->second.Contents;
for (const TextDocumentContentChangeEvent &Change : Changes) {
if (!Change.range) {
Contents = std::move(NewContents);
}
- EntryIt->second = Contents;
- return Contents;
+ updateVersion(D, Version);
+ D.Contents = std::move(Contents);
+ return D;
}
void DraftStore::removeDraft(PathRef File) {
/// 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.
+/// Each time a draft is updated, it is assigned a version number. This can be
+/// specified by the caller or incremented from the previous version.
class DraftStore {
public:
+ struct Draft {
+ std::string Contents;
+ int64_t Version = -1;
+ };
+
/// \return Contents of the stored document.
/// For untracked files, a llvm::None is returned.
- llvm::Optional<std::string> getDraft(PathRef File) const;
+ llvm::Optional<Draft> getDraft(PathRef File) const;
/// \return List of names of the drafts in this store.
std::vector<Path> getActiveFiles() const;
/// Replace contents of the draft for \p File with \p Contents.
- void addDraft(PathRef File, StringRef Contents);
+ /// If no version is specified, one will be automatically assigned.
+ /// Returns the version.
+ int64_t addDraft(PathRef File, llvm::Optional<int64_t> Version,
+ StringRef Contents);
/// Update the contents of the draft for \p File based on \p Changes.
/// If a position in \p Changes is invalid (e.g. out-of-range), the
/// draft is not modified.
+ /// If no version is specified, one will be automatically assigned.
///
/// \return The new version of the draft for \p File, or an error if the
/// changes couldn't be applied.
- llvm::Expected<std::string>
- updateDraft(PathRef File,
+ llvm::Expected<Draft>
+ updateDraft(PathRef File, llvm::Optional<int64_t> Version,
llvm::ArrayRef<TextDocumentContentChangeEvent> Changes);
/// Remove the draft from the store.
private:
mutable std::mutex Mutex;
- llvm::StringMap<std::string> Drafts;
+ llvm::StringMap<Draft> Drafts;
};
} // namespace clangd
return O && O.map("uri", R.uri);
}
+llvm::json::Value toJSON(const VersionedTextDocumentIdentifier &R) {
+ auto Result = toJSON(static_cast<const TextDocumentIdentifier &>(R));
+ if (R.version)
+ Result.getAsObject()->try_emplace("version", R.version);
+ return Result;
+}
+
+bool fromJSON(const llvm::json::Value &Params,
+ VersionedTextDocumentIdentifier &R) {
+ llvm::json::ObjectMapper O(Params);
+ return fromJSON(Params, static_cast<TextDocumentIdentifier &>(R)) && O &&
+ O.map("version", R.version);
+}
+
bool fromJSON(const llvm::json::Value &Params, Position &R) {
llvm::json::ObjectMapper O(Params);
return O && O.map("line", R.line) && O.map("character", R.character);
llvm::json::Value toJSON(const TextDocumentIdentifier &);
bool fromJSON(const llvm::json::Value &, TextDocumentIdentifier &);
+struct VersionedTextDocumentIdentifier : public TextDocumentIdentifier {
+ // The version number of this document. If a versioned text document
+ // identifier is sent from the server to the client and the file is not open
+ // in the editor (the server has not received an open notification before) the
+ // server can send `null` to indicate that the version is known and the
+ // content on disk is the master (as speced with document content ownership).
+ //
+ // The version number of a document will increase after each change, including
+ // undo/redo. The number doesn't need to be consecutive.
+ llvm::Optional<std::int64_t> version;
+};
+llvm::json::Value toJSON(const VersionedTextDocumentIdentifier &);
+bool fromJSON(const llvm::json::Value &, VersionedTextDocumentIdentifier &);
+
struct Position {
/// Line position in a document (zero-based).
int line = 0;
std::string languageId;
/// The version number of this document (it will strictly increase after each
- int version = 0;
+ std::int64_t version = 0;
/// The content of the opened text document.
std::string text;
/// The document that did change. The version number points
/// to the version after all provided content changes have
/// been applied.
- TextDocumentIdentifier textDocument;
+ VersionedTextDocumentIdentifier textDocument;
/// The actual content changes.
std::vector<TextDocumentContentChangeEvent> contentChanges;
#include "Annotations.h"
#include "DraftStore.h"
#include "SourceCode.h"
+#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
constexpr llvm::StringLiteral Path("/hello.cpp");
// Set the initial content.
- DS.addDraft(Path, InitialSrc.code());
+ EXPECT_EQ(0, DS.addDraft(Path, llvm::None, InitialSrc.code()));
for (size_t i = 1; i < Steps.size(); i++) {
Annotations SrcBefore(Steps[i - 1].Src);
Contents.str(),
};
- llvm::Expected<std::string> Result = DS.updateDraft(Path, {Event});
+ llvm::Expected<DraftStore::Draft> Result =
+ DS.updateDraft(Path, llvm::None, {Event});
ASSERT_TRUE(!!Result);
- EXPECT_EQ(*Result, SrcAfter.code());
- EXPECT_EQ(*DS.getDraft(Path), SrcAfter.code());
+ EXPECT_EQ(Result->Contents, SrcAfter.code());
+ EXPECT_EQ(DS.getDraft(Path)->Contents, SrcAfter.code());
+ EXPECT_EQ(Result->Version, static_cast<int64_t>(i));
}
}
}
// Set the initial content.
- DS.addDraft(Path, InitialSrc.code());
+ EXPECT_EQ(0, DS.addDraft(Path, llvm::None, InitialSrc.code()));
- llvm::Expected<std::string> Result = DS.updateDraft(Path, Changes);
+ llvm::Expected<DraftStore::Draft> Result =
+ DS.updateDraft(Path, llvm::None, Changes);
ASSERT_TRUE(!!Result) << llvm::toString(Result.takeError());
- EXPECT_EQ(*Result, FinalSrc.code());
- EXPECT_EQ(*DS.getDraft(Path), FinalSrc.code());
+ EXPECT_EQ(Result->Contents, FinalSrc.code());
+ EXPECT_EQ(DS.getDraft(Path)->Contents, FinalSrc.code());
+ EXPECT_EQ(Result->Version, 1);
}
TEST(DraftStoreIncrementalUpdateTest, Simple) {
DraftStore DS;
Path File = "foo.cpp";
- DS.addDraft(File, "int main() {}\n");
+ DS.addDraft(File, llvm::None, "int main() {}\n");
TextDocumentContentChangeEvent Change;
Change.range.emplace();
Change.range->end.character = 2;
Change.rangeLength = 10;
- Expected<std::string> Result = DS.updateDraft(File, {Change});
+ Expected<DraftStore::Draft> Result =
+ DS.updateDraft(File, llvm::None, {Change});
EXPECT_TRUE(!Result);
EXPECT_EQ(
DraftStore DS;
Path File = "foo.cpp";
- DS.addDraft(File, "int main() {}\n");
+ DS.addDraft(File, llvm::None, "int main() {}\n");
TextDocumentContentChangeEvent Change;
Change.range.emplace();
Change.range->end.line = 0;
Change.range->end.character = 3;
- Expected<std::string> Result = DS.updateDraft(File, {Change});
+ auto Result = DS.updateDraft(File, llvm::None, {Change});
EXPECT_TRUE(!Result);
EXPECT_EQ(toString(Result.takeError()),
DraftStore DS;
Path File = "foo.cpp";
- DS.addDraft(File, "int main() {}\n");
+ DS.addDraft(File, llvm::None, "int main() {}\n");
TextDocumentContentChangeEvent Change;
Change.range.emplace();
Change.range->end.character = 100;
Change.text = "foo";
- Expected<std::string> Result = DS.updateDraft(File, {Change});
+ auto Result = DS.updateDraft(File, llvm::None, {Change});
EXPECT_TRUE(!Result);
EXPECT_EQ(toString(Result.takeError()),
DraftStore DS;
Path File = "foo.cpp";
- DS.addDraft(File, "int main() {}\n");
+ DS.addDraft(File, llvm::None, "int main() {}\n");
TextDocumentContentChangeEvent Change;
Change.range.emplace();
Change.range->end.character = 100;
Change.text = "foo";
- Expected<std::string> Result = DS.updateDraft(File, {Change});
+ auto Result = DS.updateDraft(File, llvm::None, {Change});
EXPECT_TRUE(!Result);
EXPECT_EQ(toString(Result.takeError()),
DraftStore DS;
Path File = "foo.cpp";
- DS.addDraft(File, "int main() {}\n");
+ DS.addDraft(File, llvm::None, "int main() {}\n");
TextDocumentContentChangeEvent Change;
Change.range.emplace();
Change.range->end.character = 0;
Change.text = "foo";
- Expected<std::string> Result = DS.updateDraft(File, {Change});
+ auto Result = DS.updateDraft(File, llvm::None, {Change});
EXPECT_TRUE(!Result);
EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)");
DraftStore DS;
Path File = "foo.cpp";
- DS.addDraft(File, "int main() {}\n");
+ DS.addDraft(File, llvm::None, "int main() {}\n");
TextDocumentContentChangeEvent Change;
Change.range.emplace();
Change.range->end.character = 0;
Change.text = "foo";
- Expected<std::string> Result = DS.updateDraft(File, {Change});
+ auto Result = DS.updateDraft(File, llvm::None, {Change});
EXPECT_TRUE(!Result);
EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)");
Path File = "foo.cpp";
StringRef OriginalContents = "int main() {}\n";
- DS.addDraft(File, OriginalContents);
+ EXPECT_EQ(0, DS.addDraft(File, llvm::None, OriginalContents));
// The valid change
TextDocumentContentChangeEvent Change1;
Change2.range->end.character = 100;
Change2.text = "something";
- Expected<std::string> Result = DS.updateDraft(File, {Change1, Change2});
+ auto Result = DS.updateDraft(File, llvm::None, {Change1, Change2});
EXPECT_TRUE(!Result);
EXPECT_EQ(toString(Result.takeError()),
"utf-16 offset 100 is invalid for line 0");
- Optional<std::string> Contents = DS.getDraft(File);
+ Optional<DraftStore::Draft> Contents = DS.getDraft(File);
EXPECT_TRUE(Contents);
- EXPECT_EQ(*Contents, OriginalContents);
+ EXPECT_EQ(Contents->Contents, OriginalContents);
+ EXPECT_EQ(Contents->Version, 0);
+}
+
+TEST(DraftStore, Version) {
+ DraftStore DS;
+ Path File = "foo.cpp";
+
+ EXPECT_EQ(25, DS.addDraft(File, 25, ""));
+ EXPECT_EQ(25, DS.getDraft(File)->Version);
+
+ EXPECT_EQ(26, DS.addDraft(File, llvm::None, ""));
+ EXPECT_EQ(26, DS.getDraft(File)->Version);
+
+ // We allow versions to go backwards.
+ EXPECT_EQ(7, DS.addDraft(File, 7, ""));
+ EXPECT_EQ(7, DS.getDraft(File)->Version);
+
+ // Valid (no-op) change modifies version.
+ auto Result = DS.updateDraft(File, 10, {});
+ EXPECT_TRUE(!!Result);
+ EXPECT_EQ(10, Result->Version);
+ EXPECT_EQ(10, DS.getDraft(File)->Version);
+
+ Result = DS.updateDraft(File, llvm::None, {});
+ EXPECT_TRUE(!!Result);
+ EXPECT_EQ(11, Result->Version);
+ EXPECT_EQ(11, DS.getDraft(File)->Version);
+
+ TextDocumentContentChangeEvent InvalidChange;
+ InvalidChange.range.emplace();
+ InvalidChange.rangeLength = 99;
+
+ Result = DS.updateDraft(File, 15, {InvalidChange});
+ EXPECT_FALSE(!!Result);
+ consumeError(Result.takeError());
+ EXPECT_EQ(11, DS.getDraft(File)->Version);
}
} // namespace