Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -35,6 +35,7 @@ ExpectedTypes.cpp FindSymbols.cpp FileDistance.cpp + Format.cpp FS.cpp FSProvider.cpp FuzzyMatch.cpp Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -366,7 +366,7 @@ {"documentRangeFormattingProvider", true}, {"documentOnTypeFormattingProvider", llvm::json::Object{ - {"firstTriggerCharacter", "}"}, + {"firstTriggerCharacter", "\n"}, {"moreTriggerCharacter", {}}, }}, {"codeActionProvider", true}, @@ -583,11 +583,7 @@ "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( Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -207,10 +207,11 @@ llvm::Expected 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 - formatOnType(StringRef Code, PathRef File, Position Pos); + llvm::Expected> 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. Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -10,6 +10,7 @@ #include "ClangdUnit.h" #include "CodeComplete.h" #include "FindSymbols.h" +#include "Format.h" #include "Headers.h" #include "Protocol.h" #include "SourceCode.h" @@ -276,20 +277,23 @@ return formatCode(Code, File, {tooling::Range(0, Code.size())}); } -llvm::Expected -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> +ClangdServer::formatOnType(llvm::StringRef Code, PathRef File, Position Pos, + StringRef TriggerText) { llvm::Expected 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; + auto FS = FSProvider.getFileSystem(); + auto Style = format::getStyle(format::DefaultFormatStyle, File, + format::DefaultFallbackStyle, Code, FS.get()); + if (!Style) + return Style.takeError(); - return formatCode(Code, File, {tooling::Range(PreviousLBracePos, Len)}); + std::vector 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, Index: clangd/Format.h =================================================================== --- /dev/null +++ clangd/Format.h @@ -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 near \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, 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 +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 &Replacements); + +} // namespace clangd +} // namespace clang + +#endif + Index: clangd/Format.cpp =================================================================== --- /dev/null +++ clangd/Format.cpp @@ -0,0 +1,362 @@ +//===--- 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 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()); +} + +llvm::StringRef Filename = ""; + +// 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 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)); + + // 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 || + (!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))); + } + + // 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())); + + // The identifier improves clang-format's parsing of e.g. for without braces. + Result.CursorPlaceholder = "//==\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 +split(const tooling::Replacements &Replacements, unsigned OldCursor, + unsigned NewCursor) { + std::vector 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 +formatIncremental(llvm::StringRef OriginalCode, unsigned OriginalCursor, + llvm::StringRef InsertedText, format::FormatStyle Style) { + IncrementalChanges Incremental = + getIncrementalChanges(OriginalCode, OriginalCursor, InsertedText); + + // 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 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>{ + {"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 &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 clang Index: test/clangd/formatting.test =================================================================== --- test/clangd/formatting.test +++ test/clangd/formatting.test @@ -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":"}","options":{"tabSize":4,"insertSpaces":true}}} +{"jsonrpc":"2.0","id":5,"method":"textDocument/onTypeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"position":{"line":1,"character":0},"ch":"\n","options":{"tabSize":4,"insertSpaces":true}}} # CHECK: "id": 5, # CHECK-NEXT: "jsonrpc": "2.0", # CHECK-NEXT: "result": [ # CHECK-NEXT: { -# CHECK-NEXT: "newText": "", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 8, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 7, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "newText": " ", # CHECK-NEXT: "range": { # CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 10, +# CHECK-NEXT: "character": 5, # CHECK-NEXT: "line": 0 # CHECK-NEXT: }, # CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 9, +# 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": 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: } Index: test/clangd/initialize-params.test =================================================================== --- test/clangd/initialize-params.test +++ test/clangd/initialize-params.test @@ -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, Index: unittests/clangd/CMakeLists.txt =================================================================== --- unittests/clangd/CMakeLists.txt +++ unittests/clangd/CMakeLists.txt @@ -27,6 +27,7 @@ FileDistanceTests.cpp FileIndexTests.cpp FindSymbolsTests.cpp + FormatTests.cpp FSTests.cpp FunctionTests.cpp FuzzyMatchTests.cpp Index: unittests/clangd/FormatTests.cpp =================================================================== --- /dev/null +++ unittests/clangd/FormatTests.cpp @@ -0,0 +1,246 @@ +//===-- 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( +// 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"); +} + +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, FormatBrace) { + expectAfter("}", R"cpp( +vector x= { + 1, + 2, + 3}^ +)cpp", + R"cpp( +vector x = {1, 2, 3}^ +)cpp"); +} + +} // namespace +} // namespace clangd +} // namespace clang