[clang-tools-extra] r362939 - [clangd] Revamp textDocument/onTypeFormatting.

Sam McCall via cfe-commits cfe-commits at lists.llvm.org
Mon Jun 10 07:26:21 PDT 2019


Author: sammccall
Date: Mon Jun 10 07:26:21 2019
New Revision: 362939

URL: http://llvm.org/viewvc/llvm-project?rev=362939&view=rev
Log:
[clangd] Revamp textDocument/onTypeFormatting.

Summary:
The existing implementation (which triggers on }) is fairly simple and
has flaws:
 - doesn't trigger frequently/regularly enough (particularly in editors that type the }
 for you)
 - often reformats too much code around the edit
 - has jarring cases that I don't have clear ideas for fixing

This implementation is designed to trigger on newline, which feels to me more
intuitive than } or ;.
It does have allow for reformatting after other characters - it has a
basic behavior and a model for adding specialized behavior for
particular characters. But at least initially I'd stick to advertising
\n in the capabilities.

This also handles comment splitting: when you insert a line break inside
a line comment, it will make the new line into an aligned line comment.

Working on tests, but want people to patch it in and try it - it's hard to
see if "feel" is right purely by looking at a test.

Reviewers: ilya-biryukov, hokein

Subscribers: mgorny, ioeric, MaskRay, jkorous, arphaman, kadircet, cfe-commits

Tags: #clang

Differential Revision: https://reviews.llvm.org/D60605

Added:
    clang-tools-extra/trunk/clangd/Format.cpp
    clang-tools-extra/trunk/clangd/Format.h
    clang-tools-extra/trunk/clangd/unittests/FormatTests.cpp
Modified:
    clang-tools-extra/trunk/clangd/CMakeLists.txt
    clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp
    clang-tools-extra/trunk/clangd/ClangdServer.cpp
    clang-tools-extra/trunk/clangd/ClangdServer.h
    clang-tools-extra/trunk/clangd/test/formatting.test
    clang-tools-extra/trunk/clangd/test/initialize-params.test
    clang-tools-extra/trunk/clangd/unittests/CMakeLists.txt

Modified: clang-tools-extra/trunk/clangd/CMakeLists.txt
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/CMakeLists.txt?rev=362939&r1=362938&r2=362939&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/CMakeLists.txt (original)
+++ clang-tools-extra/trunk/clangd/CMakeLists.txt Mon Jun 10 07:26:21 2019
@@ -48,6 +48,7 @@ add_clang_library(clangDaemon
   ExpectedTypes.cpp
   FindSymbols.cpp
   FileDistance.cpp
+  Format.cpp
   FS.cpp
   FSProvider.cpp
   FormattedString.cpp

Modified: clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp?rev=362939&r1=362938&r2=362939&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp (original)
+++ clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp Mon Jun 10 07:26:21 2019
@@ -359,7 +359,7 @@ void ClangdLSPServer::onInitialize(const
             {"documentRangeFormattingProvider", true},
             {"documentOnTypeFormattingProvider",
              llvm::json::Object{
-                 {"firstTriggerCharacter", "}"},
+                 {"firstTriggerCharacter", "\n"},
                  {"moreTriggerCharacter", {}},
              }},
             {"codeActionProvider", true},
@@ -576,11 +576,7 @@ void ClangdLSPServer::onDocumentOnTypeFo
         "onDocumentOnTypeFormatting called for non-added file",
         ErrorCode::InvalidParams));
 
-  auto ReplacementsOrError = Server->formatOnType(*Code, File, Params.position);
-  if (ReplacementsOrError)
-    Reply(replacementsToEdits(*Code, ReplacementsOrError.get()));
-  else
-    Reply(ReplacementsOrError.takeError());
+  Reply(Server->formatOnType(*Code, File, Params.position, Params.ch));
 }
 
 void ClangdLSPServer::onDocumentRangeFormatting(

Modified: clang-tools-extra/trunk/clangd/ClangdServer.cpp
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/ClangdServer.cpp?rev=362939&r1=362938&r2=362939&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/ClangdServer.cpp (original)
+++ clang-tools-extra/trunk/clangd/ClangdServer.cpp Mon Jun 10 07:26:21 2019
@@ -10,6 +10,7 @@
 #include "ClangdUnit.h"
 #include "CodeComplete.h"
 #include "FindSymbols.h"
+#include "Format.h"
 #include "FormattedString.h"
 #include "Headers.h"
 #include "Protocol.h"
@@ -250,20 +251,23 @@ ClangdServer::formatFile(llvm::StringRef
   return formatCode(Code, File, {tooling::Range(0, Code.size())});
 }
 
-llvm::Expected<tooling::Replacements>
-ClangdServer::formatOnType(llvm::StringRef Code, PathRef File, Position Pos) {
-  // Look for the previous opening brace from the character position and
-  // format starting from there.
+llvm::Expected<std::vector<TextEdit>>
+ClangdServer::formatOnType(llvm::StringRef Code, PathRef File, Position Pos,
+                           StringRef TriggerText) {
   llvm::Expected<size_t> CursorPos = positionToOffset(Code, Pos);
   if (!CursorPos)
     return CursorPos.takeError();
-  size_t PreviousLBracePos =
-      llvm::StringRef(Code).find_last_of('{', *CursorPos);
-  if (PreviousLBracePos == llvm::StringRef::npos)
-    PreviousLBracePos = *CursorPos;
-  size_t Len = *CursorPos - PreviousLBracePos;
-
-  return formatCode(Code, File, {tooling::Range(PreviousLBracePos, Len)});
+  auto FS = FSProvider.getFileSystem();
+  auto Style = format::getStyle(format::DefaultFormatStyle, File,
+                                format::DefaultFallbackStyle, Code, FS.get());
+  if (!Style)
+    return Style.takeError();
+
+  std::vector<TextEdit> Result;
+  for (const tooling::Replacement &R :
+       formatIncremental(Code, *CursorPos, TriggerText, *Style))
+    Result.push_back(replacementToEdit(Code, R));
+  return Result;
 }
 
 void ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName,

Modified: clang-tools-extra/trunk/clangd/ClangdServer.h
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/ClangdServer.h?rev=362939&r1=362938&r2=362939&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/ClangdServer.h (original)
+++ clang-tools-extra/trunk/clangd/ClangdServer.h Mon Jun 10 07:26:21 2019
@@ -214,10 +214,11 @@ public:
   llvm::Expected<tooling::Replacements> formatFile(StringRef Code,
                                                    PathRef File);
 
-  /// Run formatting after a character was typed at \p Pos in \p File with
+  /// Run formatting after \p TriggerText was typed at \p Pos in \p File with
   /// content \p Code.
-  llvm::Expected<tooling::Replacements>
-  formatOnType(StringRef Code, PathRef File, Position Pos);
+  llvm::Expected<std::vector<TextEdit>> formatOnType(StringRef Code,
+                                                     PathRef File, Position Pos,
+                                                     StringRef TriggerText);
 
   /// Rename all occurrences of the symbol at the \p Pos in \p File to
   /// \p NewName.

Added: clang-tools-extra/trunk/clangd/Format.cpp
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/Format.cpp?rev=362939&view=auto
==============================================================================
--- clang-tools-extra/trunk/clangd/Format.cpp (added)
+++ clang-tools-extra/trunk/clangd/Format.cpp Mon Jun 10 07:26:21 2019
@@ -0,0 +1,377 @@
+//===--- Format.cpp -----------------------------------------*- C++-*------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+#include "Format.h"
+#include "Logger.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Format/Format.h"
+#include "clang/Lex/Lexer.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "llvm/Support/Unicode.h"
+
+namespace clang {
+namespace clangd {
+namespace {
+
+/// Append closing brackets )]} to \p Code to make it well-formed.
+/// Clang-format conservatively refuses to format files with unmatched brackets
+/// as it isn't sure where the errors are and so can't correct.
+/// When editing, it's reasonable to assume code before the cursor is complete.
+void closeBrackets(std::string &Code, const format::FormatStyle &Style) {
+  SourceManagerForFile FileSM("dummy.cpp", Code);
+  auto &SM = FileSM.get();
+  FileID FID = SM.getMainFileID();
+  Lexer Lex(FID, SM.getBuffer(FID), SM, format::getFormattingLangOpts(Style));
+  Token Tok;
+  std::vector<char> Brackets;
+  while (!Lex.LexFromRawLexer(Tok)) {
+    switch(Tok.getKind()) {
+      case tok::l_paren:
+        Brackets.push_back(')');
+        break;
+      case tok::l_brace:
+        Brackets.push_back('}');
+        break;
+      case tok::l_square:
+        Brackets.push_back(']');
+        break;
+      case tok::r_paren:
+        if (!Brackets.empty() && Brackets.back() == ')')
+          Brackets.pop_back();
+        break;
+      case tok::r_brace:
+        if (!Brackets.empty() && Brackets.back() == '}')
+          Brackets.pop_back();
+        break;
+      case tok::r_square:
+        if (!Brackets.empty() && Brackets.back() == ']')
+          Brackets.pop_back();
+        break;
+      default:
+        continue;
+    }
+  }
+  // Attempt to end any open comments first.
+  Code.append("\n// */\n");
+  Code.append(Brackets.rbegin(), Brackets.rend());
+}
+
+static StringRef commentMarker(llvm::StringRef Line) {
+  for (StringRef Marker : {"///", "//"}){
+    auto I = Line.rfind(Marker);
+    if (I != StringRef::npos)
+      return Line.substr(I, Marker.size());
+  }
+  return "";
+}
+
+llvm::StringRef firstLine(llvm::StringRef Code) {
+  return Code.take_until([](char C) { return C == '\n'; });
+}
+
+llvm::StringRef lastLine(llvm::StringRef Code) {
+  llvm::StringRef Rest = Code;
+  while (!Rest.empty() && Rest.back() != '\n')
+    Rest = Rest.drop_back();
+  return Code.substr(Rest.size());
+}
+
+// Filename is needed for tooling::Replacement and some overloads of reformat().
+// Its value should not affect the outcome. We use the default from reformat().
+llvm::StringRef Filename = "<stdin>";
+
+// tooling::Replacement from overlapping StringRefs: From must be part of Code.
+tooling::Replacement replacement(llvm::StringRef Code, llvm::StringRef From,
+                                 llvm::StringRef To) {
+  assert(From.begin() >= Code.begin() && From.end() <= Code.end());
+  // The filename is required but ignored.
+  return tooling::Replacement(Filename, From.data() - Code.data(),
+                              From.size(), To);
+};
+
+// High-level representation of incremental formatting changes.
+// The changes are made in two steps.
+// 1) a (possibly-empty) set of changes synthesized by clangd (e.g. adding
+//    comment markers when splitting a line comment with a newline).
+// 2) a selective clang-format run:
+//    - the "source code" passed to clang format is the code up to the cursor,
+//      a placeholder for the cursor, and some closing brackets
+//    - the formatting is restricted to the cursor and (possibly) other ranges
+//      (e.g. the old line when inserting a newline).
+//    - changes before the cursor are applied, those after are discarded.
+struct IncrementalChanges {
+  // Changes that should be applied before running clang-format.
+  tooling::Replacements Changes;
+  // Ranges of the original source code that should be clang-formatted.
+  // The CursorProxyText will also be formatted.
+  std::vector<tooling::Range> FormatRanges;
+  // The source code that should stand in for the cursor when clang-formatting.
+  // e.g. after inserting a newline, a line-comment at the cursor is used to
+  // ensure that the newline is preserved.
+  std::string CursorPlaceholder;
+};
+
+// After a newline:
+//  - we continue any line-comment that was split
+//  - we format the old line in addition to the cursor
+//  - we represent the cursor with a line comment to preserve the newline
+IncrementalChanges getIncrementalChangesAfterNewline(llvm::StringRef Code,
+                                                     unsigned Cursor) {
+  IncrementalChanges Result;
+  // Before newline, code looked like:
+  //    leading^trailing
+  // After newline, code looks like:
+  //    leading
+  //    indentation^trailing
+  // Where indentation was added by the editor.
+  StringRef Trailing = firstLine(Code.substr(Cursor));
+  StringRef Indentation = lastLine(Code.take_front(Cursor));
+  if (Indentation.data() == Code.data()) {
+    vlog("Typed a newline, but we're still on the first line!");
+    return Result;
+  }
+  StringRef Leading =
+      lastLine(Code.take_front(Indentation.data() - Code.data() - 1));
+  StringRef NextLine = firstLine(Code.substr(Cursor + Trailing.size() + 1));
+
+  // Strip leading whitespace on trailing line.
+  StringRef TrailingTrim = Trailing.ltrim();
+  if (unsigned TrailWS = Trailing.size() - TrailingTrim.size())
+    cantFail(Result.Changes.add(
+        replacement(Code, StringRef(Trailing.begin(), TrailWS), "")));
+
+  // If we split a comment, replace indentation with a comment marker.
+  // If the editor made the new line a comment, also respect that.
+  StringRef CommentMarker = commentMarker(Leading);
+  bool NewLineIsComment = !commentMarker(Indentation).empty();
+  if (!CommentMarker.empty() &&
+      (NewLineIsComment || !commentMarker(NextLine).empty() ||
+       (!TrailingTrim.empty() && !TrailingTrim.startswith("//")))) {
+    using llvm::sys::unicode::columnWidthUTF8;
+    // We indent the new comment to match the previous one.
+    StringRef PreComment =
+        Leading.take_front(CommentMarker.data() - Leading.data());
+    std::string IndentAndComment =
+        (std::string(columnWidthUTF8(PreComment), ' ') + CommentMarker + " ")
+            .str();
+    cantFail(
+        Result.Changes.add(replacement(Code, Indentation, IndentAndComment)));
+  } else {
+    // Remove any indentation and let clang-format re-add it.
+    // This prevents the cursor marker dragging e.g. an aligned comment with it.
+    cantFail(Result.Changes.add(replacement(Code, Indentation, "")));
+  }
+
+  // If we put a the newline inside a {} pair, put } on its own line...
+  if (CommentMarker.empty() && Leading.endswith("{") &&
+      Trailing.startswith("}")) {
+    cantFail(
+        Result.Changes.add(replacement(Code, Trailing.take_front(1), "\n}")));
+    // ...and format it.
+    Result.FormatRanges.push_back(
+        tooling::Range(Trailing.data() - Code.data() + 1, 1));
+  }
+
+  // Format the whole leading line.
+  Result.FormatRanges.push_back(
+      tooling::Range(Leading.data() - Code.data(), Leading.size()));
+
+  // We use a comment to represent the cursor, to preserve the newline.
+  // A trailing identifier improves parsing of e.g. for without braces.
+  // Exception: if the previous line has a trailing comment, we can't use one
+  // as the cursor (they will be aligned). But in this case we don't need to.
+  Result.CursorPlaceholder = !CommentMarker.empty() ? "ident" : "//==\nident";
+
+  return Result;
+}
+
+IncrementalChanges getIncrementalChanges(llvm::StringRef Code, unsigned Cursor,
+                                         llvm::StringRef InsertedText) {
+  IncrementalChanges Result;
+  if (InsertedText == "\n")
+    return getIncrementalChangesAfterNewline(Code, Cursor);
+
+  Result.CursorPlaceholder = " /**/";
+  return Result;
+}
+
+// Returns equivalent replacements that preserve the correspondence between
+// OldCursor and NewCursor. If OldCursor lies in a replaced region, that
+// replacement will be split.
+std::vector<tooling::Replacement>
+split(const tooling::Replacements &Replacements, unsigned OldCursor,
+      unsigned NewCursor) {
+  std::vector<tooling::Replacement> Result;
+  int LengthChange = 0;
+  for (const tooling::Replacement &R : Replacements) {
+    if (R.getOffset() + R.getLength() <= OldCursor) { // before cursor
+      Result.push_back(R);
+      LengthChange += R.getReplacementText().size() - R.getLength();
+    } else if (R.getOffset() < OldCursor) { // overlaps cursor
+      int ReplacementSplit = NewCursor - LengthChange - R.getOffset();
+      assert(ReplacementSplit >= 0 &&
+             ReplacementSplit <= int(R.getReplacementText().size()) &&
+             "NewCursor incompatible with OldCursor!");
+      Result.push_back(tooling::Replacement(
+          R.getFilePath(), R.getOffset(), OldCursor - R.getOffset(),
+          R.getReplacementText().take_front(ReplacementSplit)));
+      Result.push_back(tooling::Replacement(
+          R.getFilePath(), OldCursor,
+          R.getLength() - (OldCursor - R.getOffset()),
+          R.getReplacementText().drop_front(ReplacementSplit)));
+    } else if (R.getOffset() >= OldCursor) { // after cursor
+      Result.push_back(R);
+    }
+  }
+  return Result;
+}
+
+} // namespace
+
+// We're simulating the following sequence of changes:
+//   - apply the pre-formatting edits (see getIncrementalChanges)
+//   - insert a placeholder for the cursor
+//   - format some of the resulting code
+//   - remove the cursor placeholder again
+// The replacements we return are produced by composing these.
+//
+// The text we actually pass to clang-format is slightly different from this,
+// e.g. we have to close brackets. We ensure these differences are *after*
+// all the regions we want to format, and discard changes in them.
+std::vector<tooling::Replacement>
+formatIncremental(llvm::StringRef OriginalCode, unsigned OriginalCursor,
+                  llvm::StringRef InsertedText, format::FormatStyle Style) {
+  IncrementalChanges Incremental =
+      getIncrementalChanges(OriginalCode, OriginalCursor, InsertedText);
+  // Never *remove* lines in response to pressing enter! This annoys users.
+  if (InsertedText == "\n") {
+    Style.MaxEmptyLinesToKeep = 1000;
+    Style.KeepEmptyLinesAtTheStartOfBlocks = true;
+  }
+
+  // Compute the code we want to format:
+  // 1) Start with code after the pre-formatting edits.
+  std::string CodeToFormat = cantFail(
+      tooling::applyAllReplacements(OriginalCode, Incremental.Changes));
+  unsigned Cursor = Incremental.Changes.getShiftedCodePosition(OriginalCursor);
+  // 2) Truncate code after the last interesting range.
+  unsigned FormatLimit = Cursor;
+  for (tooling::Range &R : Incremental.FormatRanges)
+    FormatLimit = std::max(FormatLimit, R.getOffset() + R.getLength());
+  CodeToFormat.resize(FormatLimit);
+  // 3) Insert a placeholder for the cursor.
+  CodeToFormat.insert(Cursor, Incremental.CursorPlaceholder);
+  // 4) Append brackets after FormatLimit so the code is well-formed.
+  closeBrackets(CodeToFormat, Style);
+
+  // Determine the ranges to format:
+  std::vector<tooling::Range> RangesToFormat = Incremental.FormatRanges;
+  // Ranges after the cursor need to be adjusted for the placeholder.
+  for (auto &R : RangesToFormat) {
+    if (R.getOffset() > Cursor)
+      R = tooling::Range(R.getOffset() + Incremental.CursorPlaceholder.size(),
+                         R.getLength());
+  }
+  // We also format the cursor.
+  RangesToFormat.push_back(
+      tooling::Range(Cursor, Incremental.CursorPlaceholder.size()));
+  // Also update FormatLimit for the placeholder, we'll use this later.
+  FormatLimit += Incremental.CursorPlaceholder.size();
+
+  // Run clang-format, and truncate changes at FormatLimit.
+  tooling::Replacements FormattingChanges;
+  format::FormattingAttemptStatus Status;
+  for (const tooling::Replacement &R : format::reformat(
+           Style, CodeToFormat, RangesToFormat, Filename, &Status)) {
+    if (R.getOffset() + R.getLength() <= FormatLimit) // Before limit.
+      cantFail(FormattingChanges.add(R));
+    else if(R.getOffset() < FormatLimit) { // Overlaps limit.
+      if (R.getReplacementText().empty()) // Deletions are easy to handle.
+        cantFail(FormattingChanges.add(tooling::Replacement(Filename,
+            R.getOffset(), FormatLimit - R.getOffset(), "")));
+      else
+        // Hopefully won't happen in practice?
+        elog("Incremental clang-format edit overlapping cursor @ {0}!\n{1}",
+             Cursor, CodeToFormat);
+    }
+  }
+  if (!Status.FormatComplete)
+    vlog("Incremental format incomplete at line {0}", Status.Line);
+
+  // Now we are ready to compose the changes relative to OriginalCode.
+  //   edits -> insert placeholder -> format -> remove placeholder.
+  // We must express insert/remove as Replacements.
+  tooling::Replacements InsertCursorPlaceholder(
+      tooling::Replacement(Filename, Cursor, 0, Incremental.CursorPlaceholder));
+  unsigned FormattedCursorStart =
+               FormattingChanges.getShiftedCodePosition(Cursor),
+           FormattedCursorEnd = FormattingChanges.getShiftedCodePosition(
+               Cursor + Incremental.CursorPlaceholder.size());
+  tooling::Replacements RemoveCursorPlaceholder(
+      tooling::Replacement(Filename, FormattedCursorStart,
+                           FormattedCursorEnd - FormattedCursorStart, ""));
+
+  // We can't simply merge() and return: tooling::Replacements will combine
+  // adjacent edits left and right of the cursor. This gives the right source
+  // code, but loses information about where the cursor is!
+  // Fortunately, none of the individual passes lose information, so:
+  //  - we use merge() to compute the final Replacements
+  //  - we chain getShiftedCodePosition() to compute final cursor position
+  //  - we split the final Replacements at the cursor position, so that
+  //    each Replacement lies either before or after the cursor.
+  tooling::Replacements Final;
+  unsigned FinalCursor = OriginalCursor;
+#ifndef NDEBUG
+  std::string FinalCode = OriginalCode;
+  dlog("Initial code: {0}", FinalCode);
+#endif
+  for (auto Pass :
+       std::vector<std::pair<const char *, const tooling::Replacements *>>{
+           {"Pre-formatting changes", &Incremental.Changes},
+           {"Insert placeholder", &InsertCursorPlaceholder},
+           {"clang-format", &FormattingChanges},
+           {"Remove placeholder", &RemoveCursorPlaceholder}}) {
+    Final = Final.merge(*Pass.second);
+    FinalCursor = Pass.second->getShiftedCodePosition(FinalCursor);
+#ifndef NDEBUG
+    FinalCode =
+        cantFail(tooling::applyAllReplacements(FinalCode, *Pass.second));
+    dlog("After {0}:\n{1}^{2}", Pass.first,
+         StringRef(FinalCode).take_front(FinalCursor),
+         StringRef(FinalCode).drop_front(FinalCursor));
+#endif
+  }
+  return split(Final, OriginalCursor, FinalCursor);
+}
+
+unsigned
+transformCursorPosition(unsigned Offset,
+                        const std::vector<tooling::Replacement> &Replacements) {
+  unsigned OriginalOffset = Offset;
+  for (const auto &R : Replacements) {
+    if (R.getOffset() + R.getLength() <= OriginalOffset) {
+      // Replacement is before cursor.
+      Offset += R.getReplacementText().size();
+      Offset -= R.getLength();
+    } else if (R.getOffset() < OriginalOffset) {
+      // Replacement overlaps cursor.
+      // Preserve position within replacement text, as far as possible.
+      unsigned PositionWithinReplacement = Offset - R.getOffset();
+      if (PositionWithinReplacement > R.getReplacementText().size()) {
+        Offset += R.getReplacementText().size();
+        Offset -= PositionWithinReplacement;
+      }
+    } else {
+      // Replacement after cursor.
+      break; // Replacements are sorted, the rest are also after the cursor.
+    }
+  }
+  return Offset;
+}
+
+} // namespace clangd
+} // namespace clang

Added: clang-tools-extra/trunk/clangd/Format.h
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/Format.h?rev=362939&view=auto
==============================================================================
--- clang-tools-extra/trunk/clangd/Format.h (added)
+++ clang-tools-extra/trunk/clangd/Format.h Mon Jun 10 07:26:21 2019
@@ -0,0 +1,56 @@
+//===--- Format.h - automatic code formatting ---------------*- C++-*------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Clangd uses clang-format for formatting operations.
+// This file adapts it to support new scenarios like format-on-type.
+//
+//===----------------------------------------------------------------------===//
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMAT_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMAT_H
+
+#include "Protocol.h"
+#include "clang/Format/Format.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace clang {
+namespace clangd {
+
+/// Applies limited formatting around new \p InsertedText.
+/// The \p Code already contains the updated text before \p Cursor, and may have
+/// had additional / characters (such as indentation) inserted by the editor.
+///
+/// Example breaking a line (^ is the cursor):
+/// === before newline is typed ===
+/// if(1){^}
+/// === after newline is typed and editor indents ===
+/// if(1){
+///   ^}
+/// === after formatIncremental(InsertedText="\n") ===
+/// if (1) {
+///   ^
+/// }
+///
+/// We return sorted vector<tooling::Replacement>, not tooling::Replacements!
+/// We may insert text both before and after the cursor. tooling::Replacements
+/// would merge these, and thus lose information about cursor position.
+std::vector<tooling::Replacement>
+formatIncremental(llvm::StringRef Code, unsigned Cursor,
+                  llvm::StringRef InsertedText, format::FormatStyle Style);
+
+/// Determine the new cursor position after applying \p Replacements.
+/// Analogue of tooling::Replacements::getShiftedCodePosition().
+unsigned
+transformCursorPosition(unsigned Offset,
+                        const std::vector<tooling::Replacement> &Replacements);
+
+} // namespace clangd
+} // namespace clang
+
+#endif
+

Modified: clang-tools-extra/trunk/clangd/test/formatting.test
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/test/formatting.test?rev=362939&r1=362938&r2=362939&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/test/formatting.test (original)
+++ clang-tools-extra/trunk/clangd/test/formatting.test Mon Jun 10 07:26:21 2019
@@ -135,47 +135,34 @@
 # CHECK-NEXT:  "jsonrpc": "2.0",
 # CHECK-NEXT:  "result": []
 ---
-{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///foo.c","version":5},"contentChanges":[{"text":"int foo ( int x ) {\n  x = x + 1;\n  return x;\n}"}]}}
+{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///foo.c","version":5},"contentChanges":[{"text":"int x=\n"}]}}
 ---
-{"jsonrpc":"2.0","id":5,"method":"textDocument/onTypeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"position":{"line":3,"character":1},"ch":"}"}}
+{"jsonrpc":"2.0","id":5,"method":"textDocument/onTypeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"position":{"line":1,"character":0},"ch":"\n"}}
 #      CHECK:  "id": 5,
 # CHECK-NEXT:  "jsonrpc": "2.0",
 # CHECK-NEXT:  "result": [
 # CHECK-NEXT:    {
-# CHECK-NEXT:      "newText": "",
+# CHECK-NEXT:      "newText": " ",
 # CHECK-NEXT:      "range": {
 # CHECK-NEXT:        "end": {
-# CHECK-NEXT:          "character": 8,
+# CHECK-NEXT:          "character": 5,
 # CHECK-NEXT:          "line": 0
 # CHECK-NEXT:        },
 # CHECK-NEXT:        "start": {
-# CHECK-NEXT:          "character": 7,
+# CHECK-NEXT:          "character": 5,
 # CHECK-NEXT:          "line": 0
 # CHECK-NEXT:        }
 # CHECK-NEXT:      }
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
-# CHECK-NEXT:      "newText": "",
+# CHECK-NEXT:      "newText": "\n    ",
 # CHECK-NEXT:      "range": {
 # CHECK-NEXT:        "end": {
-# CHECK-NEXT:          "character": 10,
-# CHECK-NEXT:          "line": 0
-# CHECK-NEXT:        },
-# CHECK-NEXT:        "start": {
-# CHECK-NEXT:          "character": 9,
-# CHECK-NEXT:          "line": 0
-# CHECK-NEXT:        }
-# CHECK-NEXT:      }
-# CHECK-NEXT:    },
-# CHECK-NEXT:    {
-# CHECK-NEXT:      "newText": "",
-# CHECK-NEXT:      "range": {
-# CHECK-NEXT:        "end": {
-# CHECK-NEXT:          "character": 16,
-# CHECK-NEXT:          "line": 0
+# CHECK-NEXT:          "character": 0,
+# CHECK-NEXT:          "line": 1
 # CHECK-NEXT:        },
 # CHECK-NEXT:        "start": {
-# CHECK-NEXT:          "character": 15,
+# CHECK-NEXT:          "character": 6,
 # CHECK-NEXT:          "line": 0
 # CHECK-NEXT:        }
 # CHECK-NEXT:      }

Modified: clang-tools-extra/trunk/clangd/test/initialize-params.test
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/test/initialize-params.test?rev=362939&r1=362938&r2=362939&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/test/initialize-params.test (original)
+++ clang-tools-extra/trunk/clangd/test/initialize-params.test Mon Jun 10 07:26:21 2019
@@ -19,7 +19,7 @@
 # CHECK-NEXT:      "documentFormattingProvider": true,
 # CHECK-NEXT:      "documentHighlightProvider": true,
 # CHECK-NEXT:      "documentOnTypeFormattingProvider": {
-# CHECK-NEXT:        "firstTriggerCharacter": "}",
+# CHECK-NEXT:        "firstTriggerCharacter": "\n",
 # CHECK-NEXT:        "moreTriggerCharacter": []
 # CHECK-NEXT:      },
 # CHECK-NEXT:      "documentRangeFormattingProvider": true,

Modified: clang-tools-extra/trunk/clangd/unittests/CMakeLists.txt
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/unittests/CMakeLists.txt?rev=362939&r1=362938&r2=362939&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/unittests/CMakeLists.txt (original)
+++ clang-tools-extra/trunk/clangd/unittests/CMakeLists.txt Mon Jun 10 07:26:21 2019
@@ -39,6 +39,7 @@ add_unittest(ClangdUnitTests ClangdTests
   FileIndexTests.cpp
   FindSymbolsTests.cpp
   FormattedStringTests.cpp
+  FormatTests.cpp
   FSTests.cpp
   FunctionTests.cpp
   FuzzyMatchTests.cpp

Added: clang-tools-extra/trunk/clangd/unittests/FormatTests.cpp
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/unittests/FormatTests.cpp?rev=362939&view=auto
==============================================================================
--- clang-tools-extra/trunk/clangd/unittests/FormatTests.cpp (added)
+++ clang-tools-extra/trunk/clangd/unittests/FormatTests.cpp Mon Jun 10 07:26:21 2019
@@ -0,0 +1,308 @@
+//===-- FormatTests.cpp - Automatic code formatting tests -----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Format.h"
+#include "Annotations.h"
+#include "SourceCode.h"
+#include "TestFS.h"
+#include "clang/Format/Format.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "llvm/Support/Error.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace clang {
+namespace clangd {
+namespace {
+
+std::string afterTyped(llvm::StringRef CodeWithCursor,
+                           llvm::StringRef Typed) {
+  Annotations Code(CodeWithCursor);
+  unsigned Cursor = llvm::cantFail(positionToOffset(Code.code(), Code.point()));
+  auto Changes =
+      formatIncremental(Code.code(), Cursor, Typed,
+                        format::getGoogleStyle(format::FormatStyle::LK_Cpp));
+  tooling::Replacements Merged;
+  for (const auto& R : Changes)
+    if (llvm::Error E = Merged.add(R))
+      ADD_FAILURE() << llvm::toString(std::move(E));
+  auto NewCode = tooling::applyAllReplacements(Code.code(), Merged);
+  EXPECT_TRUE(bool(NewCode))
+      << "Bad replacements: " << llvm::toString(NewCode.takeError());
+  NewCode->insert(transformCursorPosition(Cursor, Changes), "^");
+  return *NewCode;
+}
+
+// We can't pass raw strings directly to EXPECT_EQ because of gcc bugs.
+void expectAfterNewline(const char *Before, const char *After) {
+  EXPECT_EQ(After, afterTyped(Before, "\n")) << Before;
+}
+void expectAfter(const char *Typed, const char *Before, const char *After) {
+  EXPECT_EQ(After, afterTyped(Before, Typed)) << Before;
+}
+
+TEST(FormatIncremental, SplitComment) {
+  expectAfterNewline(R"cpp(
+// this comment was
+^split
+)cpp",
+   R"cpp(
+// this comment was
+// ^split
+)cpp");
+
+  expectAfterNewline(R"cpp(
+// trailing whitespace is not a split
+^   
+)cpp",
+   R"cpp(
+// trailing whitespace is not a split
+^
+)cpp");
+
+  expectAfterNewline(R"cpp(
+// splitting a
+^
+// multiline comment
+)cpp",
+                     R"cpp(
+// splitting a
+// ^
+// multiline comment
+)cpp");
+
+  expectAfterNewline(R"cpp(
+// extra   
+    ^     whitespace
+)cpp",
+   R"cpp(
+// extra
+// ^whitespace
+)cpp");
+
+  expectAfterNewline(R"cpp(
+/// triple
+^slash
+)cpp",
+   R"cpp(
+/// triple
+/// ^slash
+)cpp");
+
+  expectAfterNewline(R"cpp(
+/// editor continuation
+//^
+)cpp",
+   R"cpp(
+/// editor continuation
+/// ^
+)cpp");
+
+  expectAfterNewline(R"cpp(
+// break before
+^ // slashes
+)cpp",
+   R"cpp(
+// break before
+^// slashes
+)cpp");
+
+
+  expectAfterNewline(R"cpp(
+int x;  // aligned
+^comment
+)cpp",
+   R"cpp(
+int x;  // aligned
+        // ^comment
+)cpp");
+
+  // Fixed bug: the second line of the aligned comment shouldn't be "attached"
+  // to the cursor and outdented.
+  expectAfterNewline(R"cpp(
+void foo() {
+  if (x)
+    return; // All spelled tokens are accounted for.
+            // that takes two lines
+            ^
+}
+)cpp",
+   R"cpp(
+void foo() {
+  if (x)
+    return;  // All spelled tokens are accounted for.
+             // that takes two lines
+  ^
+}
+)cpp");
+}
+
+TEST(FormatIncremental, Indentation) {
+  expectAfterNewline(R"cpp(
+void foo() {
+  if (bar)
+^
+)cpp",
+   R"cpp(
+void foo() {
+  if (bar)
+    ^
+)cpp");
+
+  expectAfterNewline(R"cpp(
+void foo() {
+  bar(baz(
+^
+)cpp",
+   R"cpp(
+void foo() {
+  bar(baz(
+      ^
+)cpp");
+
+  expectAfterNewline(R"cpp(
+void foo() {
+^}
+)cpp",
+   R"cpp(
+void foo() {
+  ^
+}
+)cpp");
+
+  expectAfterNewline(R"cpp(
+class X {
+protected:
+^
+)cpp",
+   R"cpp(
+class X {
+ protected:
+  ^
+)cpp");
+
+// Mismatched brackets (1)
+  expectAfterNewline(R"cpp(
+void foo() {
+  foo{bar(
+^}
+}
+)cpp",
+   R"cpp(
+void foo() {
+  foo {
+    bar(
+        ^}
+}
+)cpp");
+// Mismatched brackets (2)
+  expectAfterNewline(R"cpp(
+void foo() {
+  foo{bar(
+^text}
+}
+)cpp",
+   R"cpp(
+void foo() {
+  foo {
+    bar(
+        ^text}
+}
+)cpp");
+// Matched brackets
+  expectAfterNewline(R"cpp(
+void foo() {
+  foo{bar(
+^)
+}
+)cpp",
+   R"cpp(
+void foo() {
+  foo {
+    bar(
+        ^)
+}
+)cpp");
+}
+
+TEST(FormatIncremental, FormatPreviousLine) {
+  expectAfterNewline(R"cpp(
+void foo() {
+   untouched( );
+int x=2;
+^
+)cpp",
+                     R"cpp(
+void foo() {
+   untouched( );
+   int x = 2;
+   ^
+)cpp");
+
+  expectAfterNewline(R"cpp(
+int x=untouched( );
+auto L = []{return;return;};
+^
+)cpp",
+   R"cpp(
+int x=untouched( );
+auto L = [] {
+  return;
+  return;
+};
+^
+)cpp");
+}
+
+TEST(FormatIncremental, Annoyances) {
+  // Don't remove newlines the user typed!
+  expectAfterNewline(R"cpp(
+int x(){
+
+
+^
+}
+)cpp",
+   R"cpp(
+int x(){
+
+
+  ^
+}
+)cpp");
+  // FIXME: we should not remove newlines here, either.
+  expectAfterNewline(R"cpp(
+class x{
+ public:
+
+^
+}
+)cpp",
+   R"cpp(
+class x{
+ public:
+  ^
+}
+)cpp");
+}
+
+TEST(FormatIncremental, FormatBrace) {
+  expectAfter("}", R"cpp(
+vector<int> x= {
+  1,
+  2,
+  3}^
+)cpp",
+              R"cpp(
+vector<int> x = {1, 2, 3}^
+)cpp");
+}
+
+} // namespace
+} // namespace clangd
+} // namespace clang




More information about the cfe-commits mailing list