kadircet created this revision.
kadircet added a reviewer: sammccall.
Herald added subscribers: cfe-commits, arphaman, jkorous, MaskRay, 
ilya-biryukov.
Herald added a project: clang.

First patch for propogating multifile changes from tweak outputs to LSP
WorkspaceEdits.

Uses FS to convert tooling::Replacements to TextEdits except the MainFile.
Errors out if there are any inconsistencies between the draft version and the
version generated the edits.

We exempt MainFile from this check to not regress existing use cases, since
clangd can refactor the mainfile even if there are unsaved changes in the
editor. But for other files it will make use of on-disk state rather than
on-editor state.


Repository:
  rG LLVM Github Monorepo

https://reviews.llvm.org/D66637

Files:
  clang-tools-extra/clangd/ClangdLSPServer.cpp
  clang-tools-extra/clangd/ClangdServer.cpp
  clang-tools-extra/clangd/SourceCode.cpp
  clang-tools-extra/clangd/SourceCode.h
  clang-tools-extra/clangd/refactor/Tweak.h
  clang-tools-extra/clangd/refactor/tweaks/AnnotateHighlightings.cpp
  clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp
  clang-tools-extra/clangd/refactor/tweaks/ExpandMacro.cpp
  clang-tools-extra/clangd/refactor/tweaks/ExtractVariable.cpp
  clang-tools-extra/clangd/refactor/tweaks/RawStringLiteral.cpp
  clang-tools-extra/clangd/refactor/tweaks/SwapIfBranches.cpp
  clang-tools-extra/clangd/unittests/TweakTesting.cpp
  clang-tools-extra/clangd/unittests/TweakTests.cpp

Index: clang-tools-extra/clangd/unittests/TweakTests.cpp
===================================================================
--- clang-tools-extra/clangd/unittests/TweakTests.cpp
+++ clang-tools-extra/clangd/unittests/TweakTests.cpp
@@ -113,11 +113,15 @@
   auto Effect = apply(ID, Input);
   if (!Effect)
     return Effect.takeError();
-  if (!Effect->ApplyEdit)
+  if (!Effect->ApplyEdits)
     return llvm::createStringError(llvm::inconvertibleErrorCode(),
                                    "No replacements");
+  auto Edits = *Effect->ApplyEdits;
+  if (Edits.size() > 1)
+    return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                   "Multi-file edits");
   Annotations Code(Input);
-  return applyAllReplacements(Code.code(), *Effect->ApplyEdit);
+  return applyAllReplacements(Code.code(), Edits.begin()->second.Reps);
 }
 
 void checkTransform(llvm::StringRef ID, llvm::StringRef Input,
Index: clang-tools-extra/clangd/unittests/TweakTesting.cpp
===================================================================
--- clang-tools-extra/clangd/unittests/TweakTesting.cpp
+++ clang-tools-extra/clangd/unittests/TweakTesting.cpp
@@ -9,8 +9,8 @@
 #include "TweakTesting.h"
 
 #include "Annotations.h"
-#include "refactor/Tweak.h"
 #include "SourceCode.h"
+#include "refactor/Tweak.h"
 #include "clang/Tooling/Core/Replacement.h"
 #include "llvm/Support/Error.h"
 
@@ -98,9 +98,12 @@
     return "fail: " + llvm::toString(Result.takeError());
   if (Result->ShowMessage)
     return "message:\n" + *Result->ShowMessage;
-  if (Result->ApplyEdit) {
+  if (Result->ApplyEdits) {
+    if (Result->ApplyEdits->size() > 1)
+      return "received multi-file edits";
+    auto ApplyEdit = Result->ApplyEdits->begin()->second;
     if (auto NewText =
-            tooling::applyAllReplacements(Input.code(), *Result->ApplyEdit))
+            tooling::applyAllReplacements(Input.code(), ApplyEdit.Reps))
       return unwrap(Context, *NewText);
     else
       return "bad edits: " + llvm::toString(NewText.takeError());
Index: clang-tools-extra/clangd/refactor/tweaks/SwapIfBranches.cpp
===================================================================
--- clang-tools-extra/clangd/refactor/tweaks/SwapIfBranches.cpp
+++ clang-tools-extra/clangd/refactor/tweaks/SwapIfBranches.cpp
@@ -90,7 +90,8 @@
                                                  ElseRng->getBegin(),
                                                  ElseCode.size(), ThenCode)))
     return std::move(Err);
-  return Effect::applyEdit(Result);
+  return Effect::applyEdit(SrcMgr.getFilename(Inputs.Cursor),
+                           Edit::generateEdit(Inputs.Code, std::move(Result)));
 }
 
 } // namespace
Index: clang-tools-extra/clangd/refactor/tweaks/RawStringLiteral.cpp
===================================================================
--- clang-tools-extra/clangd/refactor/tweaks/RawStringLiteral.cpp
+++ clang-tools-extra/clangd/refactor/tweaks/RawStringLiteral.cpp
@@ -88,10 +88,13 @@
 }
 
 Expected<Tweak::Effect> RawStringLiteral::apply(const Selection &Inputs) {
-  return Effect::applyEdit(tooling::Replacements(
+  auto &SM = Inputs.AST.getSourceManager();
+  auto Reps = tooling::Replacements(
       tooling::Replacement(Inputs.AST.getSourceManager(), Str,
                            ("R\"(" + Str->getBytes() + ")\"").str(),
-                           Inputs.AST.getASTContext().getLangOpts())));
+                           Inputs.AST.getASTContext().getLangOpts()));
+  return Effect::applyEdit(SM.getFilename(Inputs.Cursor),
+                           Edit::generateEdit(Inputs.Code, std::move(Reps)));
 }
 
 } // namespace
Index: clang-tools-extra/clangd/refactor/tweaks/ExtractVariable.cpp
===================================================================
--- clang-tools-extra/clangd/refactor/tweaks/ExtractVariable.cpp
+++ clang-tools-extra/clangd/refactor/tweaks/ExtractVariable.cpp
@@ -468,7 +468,9 @@
   // replace expression with variable name
   if (auto Err = Result.add(Target->replaceWithVar(Range, VarName)))
     return std::move(Err);
-  return Effect::applyEdit(Result);
+  return Effect::applyEdit(
+      Inputs.AST.getSourceManager().getFilename(Inputs.Cursor),
+      Edit::generateEdit(Inputs.Code, std::move(Result)));
 }
 
 } // namespace
Index: clang-tools-extra/clangd/refactor/tweaks/ExpandMacro.cpp
===================================================================
--- clang-tools-extra/clangd/refactor/tweaks/ExpandMacro.cpp
+++ clang-tools-extra/clangd/refactor/tweaks/ExpandMacro.cpp
@@ -104,7 +104,7 @@
 }
 
 Expected<Tweak::Effect> ExpandMacro::apply(const Selection &Inputs) {
-  auto &SM = Inputs.AST.getASTContext().getSourceManager();
+  auto &SM = Inputs.AST.getSourceManager();
 
   std::string Replacement;
   for (const syntax::Token &T : Expansion.Expanded) {
@@ -120,11 +120,10 @@
       CharSourceRange::getCharRange(Expansion.Spelled.front().location(),
                                     Expansion.Spelled.back().endLocation());
 
-  Tweak::Effect E;
-  E.ApplyEdit.emplace();
-  llvm::cantFail(
-      E.ApplyEdit->add(tooling::Replacement(SM, MacroRange, Replacement)));
-  return E;
+  tooling::Replacements Reps;
+  llvm::cantFail(Reps.add(tooling::Replacement(SM, MacroRange, Replacement)));
+  return Effect::applyEdit(SM.getFilename(Inputs.Cursor),
+                           Edit::generateEdit(Inputs.Code, std::move(Reps)));
 }
 
 std::string ExpandMacro::title() const {
Index: clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp
===================================================================
--- clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp
+++ clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp
@@ -101,7 +101,9 @@
       Expansion(SrcMgr, CharSourceRange(CachedLocation->getSourceRange(), true),
                 PrettyTypeName);
 
-  return Tweak::Effect::applyEdit(tooling::Replacements(Expansion));
+  return Tweak::Effect::applyEdit(
+      SrcMgr.getFilename(Inputs.Cursor),
+      Edit::generateEdit(Inputs.Code, tooling::Replacements(Expansion)));
 }
 
 llvm::Error ExpandAutoType::createErrorMessage(const std::string& Message,
Index: clang-tools-extra/clangd/refactor/tweaks/AnnotateHighlightings.cpp
===================================================================
--- clang-tools-extra/clangd/refactor/tweaks/AnnotateHighlightings.cpp
+++ clang-tools-extra/clangd/refactor/tweaks/AnnotateHighlightings.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 #include "SemanticHighlighting.h"
 #include "refactor/Tweak.h"
+#include "llvm/ADT/StringRef.h"
 
 namespace clang {
 namespace clangd {
@@ -56,6 +57,7 @@
   }
   auto &SM = Inputs.AST.getSourceManager();
   tooling::Replacements Result;
+  llvm::StringRef FilePath = SM.getFilename(Inputs.Cursor);
   for (const auto &Token : HighlightingTokens) {
     assert(Token.R.start.line == Token.R.end.line &&
            "Token must be at the same line");
@@ -64,12 +66,13 @@
       return InsertOffset.takeError();
 
     auto InsertReplacement = tooling::Replacement(
-        SM.getFileEntryForID(SM.getMainFileID())->getName(), *InsertOffset, 0,
+        FilePath, *InsertOffset, 0,
         ("/* " + toTextMateScope(Token.Kind) + " */").str());
     if (auto Err = Result.add(InsertReplacement))
       return std::move(Err);
   }
-  return Effect::applyEdit(Result);
+  return Effect::applyEdit(FilePath,
+                           Edit::generateEdit(Inputs.Code, std::move(Result)));
 }
 
 } // namespace
Index: clang-tools-extra/clangd/refactor/Tweak.h
===================================================================
--- clang-tools-extra/clangd/refactor/Tweak.h
+++ clang-tools-extra/clangd/refactor/Tweak.h
@@ -24,7 +24,10 @@
 #include "Selection.h"
 #include "clang/Tooling/Core/Replacement.h"
 #include "llvm/ADT/Optional.h"
+#include "llvm/ADT/StringMap.h"
 #include "llvm/ADT/StringRef.h"
+#include "llvm/Support/SHA1.h"
+#include <array>
 namespace clang {
 namespace clangd {
 
@@ -67,12 +70,18 @@
   struct Effect {
     /// A message to be displayed to the user.
     llvm::Optional<std::string> ShowMessage;
-    /// An edit to apply to the input file.
-    llvm::Optional<tooling::Replacements> ApplyEdit;
+    /// A mapping from absolute file path to edits.
+    llvm::Optional<llvm::StringMap<Edit>> ApplyEdits;
 
-    static Effect applyEdit(tooling::Replacements R) {
+    void addEdit(StringRef FilePath, Edit Ed) {
+      assert(ApplyEdits);
+      ApplyEdits->try_emplace(FilePath, std::move(Ed));
+    }
+
+    static Effect applyEdit(StringRef FilePath, Edit Ed) {
       Effect E;
-      E.ApplyEdit = std::move(R);
+      E.ApplyEdits.emplace();
+      E.addEdit(FilePath, std::move(Ed));
       return E;
     }
     static Effect showMessage(StringRef S) {
Index: clang-tools-extra/clangd/SourceCode.h
===================================================================
--- clang-tools-extra/clangd/SourceCode.h
+++ clang-tools-extra/clangd/SourceCode.h
@@ -12,6 +12,7 @@
 //===----------------------------------------------------------------------===//
 #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_SOURCECODE_H
 #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_SOURCECODE_H
+
 #include "Context.h"
 #include "Protocol.h"
 #include "clang/Basic/Diagnostic.h"
@@ -36,6 +37,24 @@
 FileDigest digest(StringRef Content);
 Optional<FileDigest> digestFile(const SourceManager &SM, FileID FID);
 
+/// Represents a set of edits generated for a single file.
+struct Edit {
+  /// SHA1 hash of the file contents for the edits generated below. Clients
+  /// should verify contents they see are the same as this one before applying
+  /// edits.
+  std::array<uint8_t, 20> ContentDigests;
+  tooling::Replacements Reps;
+  llvm::Optional<std::vector<TextEdit>> Edits;
+
+  static Edit generateEdit(llvm::StringRef Code, tooling::Replacements Reps) {
+    Edit E;
+    E.Reps = std::move(Reps);
+    E.ContentDigests = llvm::SHA1::hash(
+        llvm::makeArrayRef(Code.bytes_begin(), Code.bytes_end()));
+    return E;
+  }
+};
+
 // This context variable controls the behavior of functions in this file
 // that convert between LSP offsets and native clang byte offsets.
 // If not set, defaults to UTF-16 for backwards-compatibility.
@@ -184,11 +203,20 @@
                                           llvm::StringRef Content,
                                           llvm::vfs::FileSystem *FS);
 
-// Cleanup and format the given replacements.
+/// Cleanup and format the given replacements.
 llvm::Expected<tooling::Replacements>
 cleanupAndFormat(StringRef Code, const tooling::Replacements &Replaces,
                  const format::FormatStyle &Style);
 
+/// Formats the edits in \p ApplyEdits and generates TextEdits from those.
+/// Ensures the files in FS are consistent with the files used while generating
+/// edits except the main file. For the mainfile it chechs the dirty versions
+/// are the same.
+llvm::Error formatAndGenerateEdits(llvm::vfs::FileSystem *FS,
+                                   llvm::StringMap<Edit> &ApplyEdits,
+                                   llvm::StringRef MainFilePath,
+                                   llvm::StringRef MainFileCode);
+
 /// Collects identifiers with counts in the source code.
 llvm::StringMap<unsigned> collectIdentifiers(llvm::StringRef Content,
                                              const format::FormatStyle &Style);
Index: clang-tools-extra/clangd/SourceCode.cpp
===================================================================
--- clang-tools-extra/clangd/SourceCode.cpp
+++ clang-tools-extra/clangd/SourceCode.cpp
@@ -11,6 +11,7 @@
 #include "FuzzyMatch.h"
 #include "Logger.h"
 #include "Protocol.h"
+#include "refactor/Tweak.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/Basic/LangOptions.h"
 #include "clang/Basic/SourceLocation.h"
@@ -19,14 +20,18 @@
 #include "clang/Format/Format.h"
 #include "clang/Lex/Lexer.h"
 #include "clang/Lex/Preprocessor.h"
+#include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/None.h"
 #include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/StringMap.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Compiler.h"
 #include "llvm/Support/Errc.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/Path.h"
+#include "llvm/Support/SHA1.h"
+#include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/Support/xxhash.h"
 #include <algorithm>
 
@@ -567,6 +572,45 @@
   return formatReplacements(Code, std::move(*CleanReplaces), Style);
 }
 
+llvm::Error formatAndGenerateEdits(llvm::vfs::FileSystem *FS,
+                                   llvm::StringMap<Edit> &ApplyEdits,
+                                   llvm::StringRef MainFilePath,
+                                   llvm::StringRef MainFileCode) {
+  for (auto &It : ApplyEdits) {
+    llvm::StringRef FilePath = It.first();
+    Edit &ApplyEdit = It.second;
+
+    llvm::StringRef Contents;
+    if (FilePath == MainFilePath) {
+      Contents = MainFileCode;
+    } else {
+      auto Buffer = FS->getBufferForFile(FilePath);
+      if (!Buffer)
+        return llvm::createStringError(
+            Buffer.getError(), "Couldn't open file %s for generating edits",
+            FilePath.data());
+      Contents = Buffer.get()->getBuffer();
+    }
+
+    if (ApplyEdit.ContentDigests !=
+        llvm::SHA1::hash(
+            llvm::makeArrayRef(Contents.bytes_begin(), Contents.size()))) {
+      return llvm::createStringError(
+          llvm::inconvertibleErrorCode(),
+          "File contents differ on disk for %s, please save", FilePath.data());
+    }
+    // FIXME: this function has I/O operations (find .clang-format file),
+    // figure out a way to cache the format style.
+    auto Style = getFormatStyleForFile(FilePath, Contents, FS);
+    if (auto NewEdits = cleanupAndFormat(Contents, ApplyEdit.Reps, Style))
+      ApplyEdit.Reps = std::move(*NewEdits);
+    else
+      elog("Failed to format reps: {0}", NewEdits.takeError());
+    ApplyEdit.Edits = replacementsToEdits(Contents, ApplyEdit.Reps);
+  }
+  return llvm::Error::success();
+}
+
 template <typename Action>
 static void lex(llvm::StringRef Code, const format::FormatStyle &Style,
                 Action A) {
Index: clang-tools-extra/clangd/ClangdServer.cpp
===================================================================
--- clang-tools-extra/clangd/ClangdServer.cpp
+++ clang-tools-extra/clangd/ClangdServer.cpp
@@ -404,16 +404,11 @@
     auto Effect = (*A)->apply(*Selection);
     if (!Effect)
       return CB(Effect.takeError());
-    if (Effect->ApplyEdit) {
-      // FIXME: this function has I/O operations (find .clang-format file),
-      // figure out a way to cache the format style.
-      auto Style = getFormatStyleForFile(File, InpAST->Inputs.Contents,
-                                         InpAST->Inputs.FS.get());
-      if (auto Formatted = cleanupAndFormat(InpAST->Inputs.Contents,
-                                            *Effect->ApplyEdit, Style))
-        Effect->ApplyEdit = std::move(*Formatted);
-      else
-        elog("Failed to format replacements: {0}", Formatted.takeError());
+    if (Effect->ApplyEdits) {
+      if (llvm::Error Err = formatAndGenerateEdits(InpAST->Inputs.FS.get(),
+                                                   *Effect->ApplyEdits, File,
+                                                   InpAST->Inputs.Contents))
+        return CB(std::move(Err));
     }
     return CB(std::move(*Effect));
   };
Index: clang-tools-extra/clangd/ClangdLSPServer.cpp
===================================================================
--- clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -24,6 +24,7 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/Path.h"
+#include "llvm/Support/SHA1.h"
 #include "llvm/Support/ScopedPrinter.h"
 
 namespace clang {
@@ -627,7 +628,7 @@
       if (!R)
         return Reply(R.takeError());
 
-      assert(R->ShowMessage || (R->ApplyEdit && "tweak has no effect"));
+      assert(R->ShowMessage || (R->ApplyEdits && "tweak has no effect"));
 
       if (R->ShowMessage) {
         ShowMessageParams Msg;
@@ -635,10 +636,21 @@
         Msg.type = MessageType::Info;
         notify("window/showMessage", Msg);
       }
-      if (R->ApplyEdit) {
+      if (R->ApplyEdits) {
         WorkspaceEdit WE;
         WE.changes.emplace();
-        (*WE.changes)[File.uri()] = replacementsToEdits(Code, *R->ApplyEdit);
+        for (const auto &It : *R->ApplyEdits) {
+          assert(It.second.Edits && "TextEdits hasn't been generated?");
+          if (auto Draft = DraftMgr.getDraft(It.first())) {
+            llvm::StringRef Contents = *Draft;
+            if (llvm::SHA1::hash(llvm::makeArrayRef(Contents.bytes_begin(),
+                                                    Contents.size())) !=
+                It.second.ContentDigests)
+              return Reply((It.first() + " needs to be saved").str());
+          }
+          (*WE.changes)[URI::create(It.first()).toString()] =
+              std::move(*It.second.Edits);
+        }
         // ApplyEdit will take care of calling Reply().
         return ApplyEdit(std::move(WE), "Tweak applied.", std::move(Reply));
       }
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to