https://github.com/erbsland-dev created 
https://github.com/llvm/llvm-project/pull/183960

Resolves #183802.

Introduce a new optional `EmptyLines` key for entries in `IncludeCategories`, 
allowing configuration of the number of empty lines inserted between include 
groups.

Add tests to validate the new behavior and update the documentation.

>From 52c0b4524e90df56f310d75c1c4ea2d64df3f53b Mon Sep 17 00:00:00 2001
From: ErbslandDEV <[email protected]>
Date: Sat, 28 Feb 2026 16:58:39 +0100
Subject: [PATCH] [clang-format] Support variable empty lines between include
 categories

Resolves #183802.

Introduce a new optional `EmptyLines` key for entries in
`IncludeCategories`, allowing configuration of the number of empty
lines inserted between include groups.

Add tests to validate the new behavior and update the documentation.
---
 clang/docs/ClangFormatStyleOptions.rst        |  13 +++
 .../clang/Tooling/Inclusions/IncludeStyle.h   |  12 +++
 clang/lib/Format/Format.cpp                   |  42 ++++++--
 clang/lib/Tooling/Inclusions/IncludeStyle.cpp |   1 +
 clang/unittests/Format/ConfigParseTest.cpp    |   9 ++
 clang/unittests/Format/SortIncludesTest.cpp   | 100 +++++++++++++-----
 6 files changed, 139 insertions(+), 38 deletions(-)

diff --git a/clang/docs/ClangFormatStyleOptions.rst 
b/clang/docs/ClangFormatStyleOptions.rst
index ed4e2aaa26052..0615c0a8aeda0 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -4417,6 +4417,18 @@ the configuration (without a prefix: ``Auto``).
   Each regular expression can be marked as case sensitive with the field
   ``CaseSensitive``, per default it is not.
 
+  There is a fourth and optional field ``EmptyLines`` that defines how many
+  empty lines are inserted before this category when ``IncludeBlocks`` is
+  ``IBS_Regroup``. The default is ``1``. ``EmptyLines: 0`` can be used to
+  suppress separation.
+
+  When regrouping jumps over categories that are not present in the file,
+  clang-format uses the maximum ``EmptyLines`` value of all category
+  priorities between the previous and the next emitted category.
+
+  ``MaxEmptyLinesToKeep`` still applies to the final number of consecutive
+  empty lines kept in the formatted output.
+
   To configure this in the .clang-format file, use:
 
   .. code-block:: yaml
@@ -4430,6 +4442,7 @@ the configuration (without a prefix: ``Auto``).
         Priority:        3
       - Regex:           '<[[:alnum:].]+>'
         Priority:        4
+        EmptyLines:      2
       - Regex:           '.*'
         Priority:        1
         SortPriority:    0
diff --git a/clang/include/clang/Tooling/Inclusions/IncludeStyle.h 
b/clang/include/clang/Tooling/Inclusions/IncludeStyle.h
index bf060617deec7..ad091994432a7 100644
--- a/clang/include/clang/Tooling/Inclusions/IncludeStyle.h
+++ b/clang/include/clang/Tooling/Inclusions/IncludeStyle.h
@@ -63,8 +63,11 @@ struct IncludeStyle {
     int SortPriority;
     /// If the regular expression is case sensitive.
     bool RegexIsCaseSensitive;
+    /// The number of blank lines to add *before* this category.
+    int EmptyLines = 1;
     bool operator==(const IncludeCategory &Other) const {
       return Regex == Other.Regex && Priority == Other.Priority &&
+             EmptyLines == Other.EmptyLines &&
              RegexIsCaseSensitive == Other.RegexIsCaseSensitive;
     }
   };
@@ -99,6 +102,14 @@ struct IncludeStyle {
   /// Each regular expression can be marked as case sensitive with the field
   /// ``CaseSensitive``, per default it is not.
   ///
+  /// There is a fourth and optional field ``EmptyLines`` that defines how
+  /// many empty lines are inserted before this category when
+  /// ``IncludeBlocks = IBS_Regroup``. The default is ``1``.
+  ///
+  /// If regrouping jumps over categories that are not present in the file,
+  /// the maximum ``EmptyLines`` value of all category priorities between the
+  /// two emitted categories is used.
+  ///
   /// To configure this in the .clang-format file, use:
   /// \code{.yaml}
   ///   IncludeCategories:
@@ -110,6 +121,7 @@ struct IncludeStyle {
   ///       Priority:        3
   ///     - Regex:           '<[[:alnum:].]+>'
   ///       Priority:        4
+  ///       EmptyLines:      2
   ///     - Regex:           '.*'
   ///       Priority:        1
   ///       SortPriority:    0
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 2f67ec86b101a..c4fd8d8246e66 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -1808,8 +1808,8 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind 
Language) {
   LLVMStyle.IfMacros.push_back("KJ_IF_MAYBE");
   LLVMStyle.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Preserve;
   LLVMStyle.IncludeStyle.IncludeCategories = {
-      {"^\"(llvm|llvm-c|clang|clang-c)/", 2, 0, false},
-      {"^(<|\"(gtest|gmock|isl|json)/)", 3, 0, false},
+      {"^\"(llvm|llvm-c|clang|clang-c)/", 2, 0, false, 1},
+      {"^(<|\"(gtest|gmock|isl|json)/)", 3, 0, false, 1},
       {".*", 1, 0, false}};
   LLVMStyle.IncludeStyle.IncludeIsMainRegex = "(Test)?$";
   LLVMStyle.IncludeStyle.MainIncludeChar = tooling::IncludeStyle::MICD_Quote;
@@ -1966,10 +1966,11 @@ FormatStyle getGoogleStyle(FormatStyle::LanguageKind 
Language) {
   GoogleStyle.AttributeMacros.push_back("absl_nullability_unknown");
   GoogleStyle.BreakTemplateDeclarations = FormatStyle::BTDS_Yes;
   GoogleStyle.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
-  GoogleStyle.IncludeStyle.IncludeCategories = {{"^<ext/.*\\.h>", 2, 0, false},
-                                                {"^<.*\\.h>", 1, 0, false},
-                                                {"^<.*", 2, 0, false},
-                                                {".*", 3, 0, false}};
+  GoogleStyle.IncludeStyle.IncludeCategories = {
+    {"^<ext/.*\\.h>", 2, 0, false, 1},
+    {"^<.*\\.h>", 1, 0, false, 1},
+    {"^<.*", 2, 0, false, 1},
+    {".*", 3, 0, false, 1}};
   GoogleStyle.IncludeStyle.IncludeIsMainRegex = "([-_](test|unittest))?$";
   GoogleStyle.IndentCaseLabels = true;
   GoogleStyle.KeepEmptyLines.AtStartOfBlock = false;
@@ -3535,6 +3536,25 @@ static void sortCppIncludes(const FormatStyle &Style,
                              }),
                 Indices.end());
 
+  const auto GetEmptyLines = [&](int LastCategory, int NewCategory) -> int {
+    if (LastCategory == NewCategory) {
+      return 0;
+    }
+    const int LowerBound = std::min(LastCategory, NewCategory);
+    const int UpperBound = std::max(LastCategory, NewCategory);
+    int EmptyLines = 1;
+    for (const auto &IncludeCategory : Style.IncludeStyle.IncludeCategories) {
+      if (IncludeCategory.Priority <= LowerBound) {
+        continue;
+      }
+      if (IncludeCategory.Priority > UpperBound) {
+        break;
+      }
+      EmptyLines = std::max(EmptyLines, IncludeCategory.EmptyLines);
+    }
+    return EmptyLines;
+  };
+
   int CurrentCategory = Includes.front().Category;
 
   // If the #includes are out of order, we generate a single replacement fixing
@@ -3551,18 +3571,22 @@ static void sortCppIncludes(const FormatStyle &Style,
   const auto OldCursor = Cursor ? *Cursor : 0;
   std::string result;
   for (unsigned Index : Indices) {
+    const auto NewCategory = Includes[Index].Category;
     if (!result.empty()) {
       result += "\n";
       if (Style.IncludeStyle.IncludeBlocks ==
               tooling::IncludeStyle::IBS_Regroup &&
-          CurrentCategory != Includes[Index].Category) {
-        result += "\n";
+          CurrentCategory != NewCategory) {
+        const int EmptyLineCount = GetEmptyLines(CurrentCategory, NewCategory);
+        for (int Count = 0; Count < EmptyLineCount; ++Count) {
+          result += "\n";
+        }
       }
     }
     result += Includes[Index].Text;
     if (Cursor && CursorIndex == Index)
       *Cursor = IncludesBeginOffset + result.size() - CursorToEOLOffset;
-    CurrentCategory = Includes[Index].Category;
+    CurrentCategory = NewCategory;
   }
 
   if (Cursor && *Cursor >= IncludesEndOffset)
diff --git a/clang/lib/Tooling/Inclusions/IncludeStyle.cpp 
b/clang/lib/Tooling/Inclusions/IncludeStyle.cpp
index 05dfb50589de0..493475382ac4f 100644
--- a/clang/lib/Tooling/Inclusions/IncludeStyle.cpp
+++ b/clang/lib/Tooling/Inclusions/IncludeStyle.cpp
@@ -19,6 +19,7 @@ void MappingTraits<IncludeStyle::IncludeCategory>::mapping(
   IO.mapOptional("Priority", Category.Priority);
   IO.mapOptional("SortPriority", Category.SortPriority);
   IO.mapOptional("CaseSensitive", Category.RegexIsCaseSensitive);
+  IO.mapOptional("EmptyLines", Category.EmptyLines, 1);
 }
 
 void ScalarEnumerationTraits<IncludeStyle::IncludeBlocksStyle>::enumeration(
diff --git a/clang/unittests/Format/ConfigParseTest.cpp 
b/clang/unittests/Format/ConfigParseTest.cpp
index f270602f32604..17a33fc6e487d 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -1054,6 +1054,15 @@ TEST(ConfigParseTest, ParsesConfiguration) {
               "    Priority: 1\n"
               "    CaseSensitive: true",
               IncludeStyle.IncludeCategories, ExpectedCategories);
+  ExpectedCategories = {{"abc/.*", 2, 0, false, 2}, {".*", 1, 0, true, 1}};
+  CHECK_PARSE("IncludeCategories:\n"
+              "  - Regex: abc/.*\n"
+              "    Priority: 2\n"
+              "    EmptyLines: 2\n"
+              "  - Regex: .*\n"
+              "    Priority: 1\n"
+              "    CaseSensitive: true",
+              IncludeStyle.IncludeCategories, ExpectedCategories);
   CHECK_PARSE("IncludeIsMainRegex: 'abc$'", IncludeStyle.IncludeIsMainRegex,
               "abc$");
   CHECK_PARSE("IncludeIsMainSourceRegex: 'abc$'",
diff --git a/clang/unittests/Format/SortIncludesTest.cpp 
b/clang/unittests/Format/SortIncludesTest.cpp
index 48ecd5d32d034..0b0408e172bbb 100644
--- a/clang/unittests/Format/SortIncludesTest.cpp
+++ b/clang/unittests/Format/SortIncludesTest.cpp
@@ -87,19 +87,19 @@ TEST_F(SortIncludesTest, TrailingComments) {
 TEST_F(SortIncludesTest, SortedIncludesUsingSortPriorityAttribute) {
   FmtStyle.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
   FmtStyle.IncludeStyle.IncludeCategories = {
-      {"^<sys/param\\.h>", 1, 0, false},
-      {"^<sys/types\\.h>", 1, 1, false},
-      {"^<sys.*/", 1, 2, false},
-      {"^<uvm/", 2, 3, false},
-      {"^<machine/", 3, 4, false},
-      {"^<dev/", 4, 5, false},
-      {"^<net.*/", 5, 6, false},
-      {"^<protocols/", 5, 7, false},
-      {"^<(fs|miscfs|msdosfs|nfs|ntfs|ufs)/", 6, 8, false},
-      {"^<(x86|amd64|i386|xen)/", 7, 8, false},
-      {"<path", 9, 11, false},
-      {"^<[^/].*\\.h>", 8, 10, false},
-      {"^\".*\\.h\"", 10, 12, false}};
+      {"^<sys/param\\.h>", 1, 0, false, 1},
+      {"^<sys/types\\.h>", 1, 1, false, 1},
+      {"^<sys.*/", 1, 2, false, 1},
+      {"^<uvm/", 2, 3, false, 1},
+      {"^<machine/", 3, 4, false, 1},
+      {"^<dev/", 4, 5, false, 1},
+      {"^<net.*/", 5, 6, false, 1},
+      {"^<protocols/", 5, 7, false, 1},
+      {"^<(fs|miscfs|msdosfs|nfs|ntfs|ufs)/", 6, 8, false, 1},
+      {"^<(x86|amd64|i386|xen)/", 7, 8, false, 1},
+      {"<path", 9, 11, false, 1},
+      {"^<[^/].*\\.h>", 8, 10, false, 1},
+      {"^\".*\\.h\"", 10, 12, false, 1}};
   verifyFormat("#include <sys/param.h>\n"
                "#include <sys/types.h>\n"
                "#include <sys/ioctl.h>\n"
@@ -627,6 +627,48 @@ TEST_F(SortIncludesTest, 
MainHeaderIsSeparatedWhenRegroupping) {
                     "a.cc"));
 }
 
+TEST_F(SortIncludesTest, EmptyLinesUseMaxAcrossSkippedCategories) {
+  Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
+  FmtStyle.MaxEmptyLinesToKeep = 2;
+  Style.IncludeCategories = {
+      {"^\"b", 1, 0, false, 2},
+      {"^\"c", 2, 0, false, 1},
+      {"^\"d", 3, 0, false, 2},
+      {"^\"e", 4, 0, false, 1},
+  };
+
+  verifyFormat("#include \"input.h\"\n"
+               "\n"
+               "\n"
+               "#include \"c.h\"\n"
+               "\n"
+               "\n"
+               "#include \"e.h\"\n",
+               sort("#include \"e.h\"\n"
+                    "#include \"c.h\"\n"
+                    "#include \"input.h\"\n",
+                    "input.cpp"),
+               FmtStyle);
+}
+
+TEST_F(SortIncludesTest, EmptyLinesCanSeparateMainHeaderByTwoBlankLines) {
+  Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
+  FmtStyle.MaxEmptyLinesToKeep = 2;
+  Style.IncludeCategories = {
+      {"^\"project/.*\"", 1, 0, false, 2},
+      {".*", 2, 0, false, 1},
+  };
+
+  verifyFormat("#include \"input.h\"\n"
+               "\n"
+               "\n"
+               "#include \"project/detail.h\"\n",
+               sort("#include \"project/detail.h\"\n"
+                    "#include \"input.h\"\n",
+                    "input.cpp"),
+               FmtStyle);
+}
+
 TEST_F(SortIncludesTest, SupportOptionalCaseSensitiveSorting) {
   FmtStyle.SortIncludes.IgnoreCase = true;
 
@@ -644,7 +686,7 @@ TEST_F(SortIncludesTest, 
SupportOptionalCaseSensitiveSorting) {
 
   Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
   Style.IncludeCategories = {
-      {"^\"", 1, 0, false}, {"^<.*\\.h>$", 2, 0, false}, {"^<", 3, 0, false}};
+      {"^\"", 1, 0, false, 1}, {"^<.*\\.h>$", 2, 0, false}, {"^<", 3, 0, 
false, 1}};
 
   StringRef UnsortedCode = "#include \"qt.h\"\n"
                            "#include <algorithm>\n"
@@ -693,11 +735,11 @@ TEST_F(SortIncludesTest, SupportCaseInsensitiveMatching) {
 
 TEST_F(SortIncludesTest, SupportOptionalCaseSensitiveMachting) {
   Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
-  Style.IncludeCategories = {{"^\"", 1, 0, false},
-                             {"^<.*\\.h>$", 2, 0, false},
-                             {"^<Q[A-Z][^\\.]*>", 3, 0, false},
-                             {"^<Qt[^\\.]*>", 4, 0, false},
-                             {"^<", 5, 0, false}};
+  Style.IncludeCategories = {{"^\"", 1, 0, false, 1},
+                             {"^<.*\\.h>$", 2, 0, false, 1},
+                             {"^<Q[A-Z][^\\.]*>", 3, 0, false, 1},
+                             {"^<Qt[^\\.]*>", 4, 0, false, 1},
+                             {"^<", 5, 0, false, 1}};
 
   StringRef UnsortedCode = "#include <QWidget>\n"
                            "#include \"qt.h\"\n"
@@ -742,8 +784,8 @@ TEST_F(SortIncludesTest, 
SupportOptionalCaseSensitiveMachting) {
 }
 
 TEST_F(SortIncludesTest, NegativePriorities) {
-  Style.IncludeCategories = {{".*important_os_header.*", -1, 0, false},
-                             {".*", 1, 0, false}};
+  Style.IncludeCategories = {{".*important_os_header.*", -1, 0, false, 1},
+                             {".*", 1, 0, false, 1}};
   verifyFormat("#include \"important_os_header.h\"\n"
                "#include \"c_main.h\"\n"
                "#include \"a_other.h\"",
@@ -763,8 +805,8 @@ TEST_F(SortIncludesTest, NegativePriorities) {
 }
 
 TEST_F(SortIncludesTest, PriorityGroupsAreSeparatedWhenRegroupping) {
-  Style.IncludeCategories = {{".*important_os_header.*", -1, 0, false},
-                             {".*", 1, 0, false}};
+  Style.IncludeCategories = {{".*important_os_header.*", -1, 0, false, 1},
+                             {".*", 1, 0, false, 1}};
   Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
 
   verifyFormat("#include \"important_os_header.h\"\n"
@@ -824,7 +866,7 @@ TEST_F(SortIncludesTest,
   Style.IncludeBlocks = Style.IBS_Regroup;
   FmtStyle.LineEnding = FormatStyle::LE_CRLF;
   Style.IncludeCategories = {
-      {"^\"a\"", 0, 0, false}, {"^\"b\"", 1, 1, false}, {".*", 2, 2, false}};
+      {"^\"a\"", 0, 0, false, 1}, {"^\"b\"", 1, 1, false}, {".*", 2, 2, false, 
1}};
   StringRef Code = "#include \"a\"\r\n" // Start of line: 0
                    "\r\n"               // Start of line: 14
                    "#include \"b\"\r\n" // Start of line: 16
@@ -847,7 +889,7 @@ TEST_F(
     
CalculatesCorrectCursorPositionWhenRemoveLinesReplacementsWithRegroupingAndCRLF)
 {
   Style.IncludeBlocks = Style.IBS_Regroup;
   FmtStyle.LineEnding = FormatStyle::LE_CRLF;
-  Style.IncludeCategories = {{".*", 0, 0, false}};
+  Style.IncludeCategories = {{".*", 0, 0, false, 1}};
   StringRef Code = "#include \"a\"\r\n"     // Start of line: 0
                    "\r\n"                   // Start of line: 14
                    "#include \"b\"\r\n"     // Start of line: 16
@@ -882,7 +924,7 @@ TEST_F(
   Style.IncludeBlocks = Style.IBS_Regroup;
   FmtStyle.LineEnding = FormatStyle::LE_CRLF;
   Style.IncludeCategories = {
-      {"^\"a\"", 0, 0, false}, {"^\"b\"", 1, 1, false}, {".*", 2, 2, false}};
+      {"^\"a\"", 0, 0, false, 1}, {"^\"b\"", 1, 1, false, 1}, {".*", 2, 2, 
false, 1}};
   StringRef Code = "#include \"a\"\r\n"     // Start of line: 0
                    "#include \"b\"\r\n"     // Start of line: 14
                    "#include \"c\"\r\n"     // Start of line: 28
@@ -909,7 +951,7 @@ TEST_F(
   Style.IncludeBlocks = Style.IBS_Regroup;
   FmtStyle.LineEnding = FormatStyle::LE_CRLF;
   Style.IncludeCategories = {
-      {"^\"a\"", 0, 0, false}, {"^\"b\"", 1, 1, false}, {".*", 2, 2, false}};
+      {"^\"a\"", 0, 0, false, 1}, {"^\"b\"", 1, 1, false, 1}, {".*", 2, 2, 
false, 1}};
   StringRef Code = "#include \"a\"\r\n"     // Start of line: 0
                    "\r\n"                   // Start of line: 14
                    "#include \"c\"\r\n"     // Start of line: 16
@@ -1157,7 +1199,7 @@ TEST_F(SortIncludesTest, 
MainIncludeCharAnyPickAngleBracket) {
 
 TEST_F(SortIncludesTest, MainIncludeCharQuoteAndRegroup) {
   Style.IncludeCategories = {
-      {"lib-a", 1, 0, false}, {"lib-b", 2, 0, false}, {"lib-c", 3, 0, false}};
+      {"lib-a", 1, 0, false, 1}, {"lib-b", 2, 0, false}, {"lib-c", 3, 0, 
false, 1}};
   Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
   Style.MainIncludeChar = tooling::IncludeStyle::MICD_Quote;
 
@@ -1187,7 +1229,7 @@ TEST_F(SortIncludesTest, MainIncludeCharQuoteAndRegroup) {
 
 TEST_F(SortIncludesTest, MainIncludeCharAngleBracketAndRegroup) {
   Style.IncludeCategories = {
-      {"lib-a", 1, 0, false}, {"lib-b", 2, 0, false}, {"lib-c", 3, 0, false}};
+      {"lib-a", 1, 0, false, 1}, {"lib-b", 2, 0, false}, {"lib-c", 3, 0, 
false, 1}};
   Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
   Style.MainIncludeChar = tooling::IncludeStyle::MICD_AngleBracket;
 

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to