Skip to content

Commit f70155a

Browse files
ZacSweersmeta-codesync[bot]
authored andcommitted
Default to preserving lambda line breaks (Kotlin#614)
Summary: This is a continuation of Kotlin#574 with the updated option control to orient around the requested `--preserve-lambda-breaks` simple option. With this, lambda block line breaks are simply just respected rather than have ktfmt impose anything. Aside from finishing up the previous PR, this also adds a setting to the IJ plugin. I cherrypicked the original commits to preserve authorship history (CC loganj) Resolves Kotlin#547 **Edit** After chatting on slack I updated the PR with a couple things: - Added a builder API for CLI tools to use in the future to avoid ABI breakages - Flipped the semantics to enable this by default, but can revert if you want to keep the old setup. I do think enabled by default is good Pull Request resolved: Kotlin#614 Reviewed By: ermattt Differential Revision: D105690093 Pulled By: hick209 fbshipit-source-id: 8a2a3232329a796a8f777d7bf7ef96016651ef61
1 parent 79562cd commit f70155a

10 files changed

Lines changed: 460 additions & 12 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea/
2+
.kotlin/
23
*.ims
34
*.iml
45

@@ -23,3 +24,4 @@ release-ktfmt-website/
2324
.gradle
2425
**/build/
2526
!src/**/build/
27+
.claude/

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
1717
- Support for lists within quoted blocks in KDoc comments (https://github.com/facebook/ktfmt/commit/68fa1585b759ad4b12ca4802bccd297f6a33b0f3)
1818
- Fix `ONLY_ADD` trailing commas strategy causing lines over MAX_WIDTH length (https://github.com/facebook/ktfmt/issues/610)
1919
- Remove forced breaking of `context` function types (https://github.com/facebook/ktfmt/pull/613)
20+
- Preserve user-authored line breaks inside lambda bodies by default, rather than have ktfmt impose anything. This can be particularly useful for DSL syntax like Compose UI or Kotlin Gradle script. The behavior follows the `FormattingOptions.preserveLambdaBreaks` setting of the chosen style. (https://github.com/facebook/ktfmt/pull/614)
21+
- Add a `FormattingOptions.Builder` API for tools to avoid breaking ABI changes with new options. (https://github.com/facebook/ktfmt/pull/614)
2022

2123
## [0.62]
2224
### Added

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,28 @@ following subset of editorconfig properties:
8888
| `ij_continuation_indent_size` | will override the continuation indent |
8989
| `ktfmt_trailing_comma_management_strategy` | one of `none`, `only_add` or `complete`<br/>will override the trailing comma management strategy |
9090

91+
#### Preserving Lambda Line Breaks
92+
93+
By default, `ktfmt` respects user-authored line breaks inside lambda bodies. This is useful for DSLs
94+
where nesting carries semantic meaning (Jetpack Compose UI hierarchies, Kotlin Gradle scripts, etc.)
95+
without ktfmt prescribing a specific outcome.
96+
97+
A lambda body whose source spans multiple lines stays multi-line; one that fits on a single line
98+
stays single-line if written that way. For example, the following is left as-is:
99+
100+
```kotlin
101+
// Multi-line in source is preserved
102+
App {
103+
SelectableCard {
104+
Button { Text("Click me") }
105+
}
106+
}
107+
108+
// Single-line in source is also preserved
109+
dependencies { implementation(libs.androidx.activity) }
110+
val state = remember { mutableStateOf(false) }
111+
```
112+
91113
***Note:***
92114
*There is no configurability as to the formatter's algorithm for formatting (apart from the different styles
93115
or limited `.editorconfig` support). This is a deliberate design decision to unify our code formatting on a

core/api/ktfmt.api

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,12 @@ public final class com/facebook/ktfmt/format/Formatter {
122122

123123
public final class com/facebook/ktfmt/format/FormattingOptions {
124124
public static final field Companion Lcom/facebook/ktfmt/format/FormattingOptions$Companion;
125+
public static final field DEFAULT_BLOCK_INDENT I
126+
public static final field DEFAULT_CONTINUATION_INDENT I
125127
public static final field DEFAULT_MAX_WIDTH I
126128
public fun <init> (IIILcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;ZZ)V
127-
public synthetic fun <init> (IIILcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
129+
public fun <init> (IIILcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;ZZZ)V
130+
public synthetic fun <init> (IIILcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
128131
public fun <init> (IIIZZZ)V
129132
public synthetic fun <init> (IIIZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
130133
public final fun component1 ()I
@@ -133,19 +136,34 @@ public final class com/facebook/ktfmt/format/FormattingOptions {
133136
public final fun component4 ()Lcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;
134137
public final fun component5 ()Z
135138
public final fun component6 ()Z
136-
public final fun copy (IIILcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;ZZ)Lcom/facebook/ktfmt/format/FormattingOptions;
137-
public static synthetic fun copy$default (Lcom/facebook/ktfmt/format/FormattingOptions;IIILcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;ZZILjava/lang/Object;)Lcom/facebook/ktfmt/format/FormattingOptions;
139+
public final fun component7 ()Z
140+
public final fun copy (IIILcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;ZZZ)Lcom/facebook/ktfmt/format/FormattingOptions;
141+
public static synthetic fun copy$default (Lcom/facebook/ktfmt/format/FormattingOptions;IIILcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;ZZZILjava/lang/Object;)Lcom/facebook/ktfmt/format/FormattingOptions;
138142
public fun equals (Ljava/lang/Object;)Z
139143
public final fun getBlockIndent ()I
140144
public final fun getContinuationIndent ()I
141145
public final fun getDebuggingPrintOpsAfterFormatting ()Z
142146
public final fun getMaxWidth ()I
147+
public final fun getPreserveLambdaBreaks ()Z
143148
public final fun getRemoveUnusedImports ()Z
144149
public final fun getTrailingCommaManagementStrategy ()Lcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;
145150
public fun hashCode ()I
151+
public final fun toBuilder ()Lcom/facebook/ktfmt/format/FormattingOptions$Builder;
146152
public fun toString ()Ljava/lang/String;
147153
}
148154

155+
public final class com/facebook/ktfmt/format/FormattingOptions$Builder {
156+
public fun <init> ()V
157+
public final fun blockIndent (I)Lcom/facebook/ktfmt/format/FormattingOptions$Builder;
158+
public final fun build ()Lcom/facebook/ktfmt/format/FormattingOptions;
159+
public final fun continuationIndent (I)Lcom/facebook/ktfmt/format/FormattingOptions$Builder;
160+
public final fun debuggingPrintOpsAfterFormatting (Z)Lcom/facebook/ktfmt/format/FormattingOptions$Builder;
161+
public final fun maxWidth (I)Lcom/facebook/ktfmt/format/FormattingOptions$Builder;
162+
public final fun preserveLambdaBreaks (Z)Lcom/facebook/ktfmt/format/FormattingOptions$Builder;
163+
public final fun removeUnusedImports (Z)Lcom/facebook/ktfmt/format/FormattingOptions$Builder;
164+
public final fun trailingCommaManagementStrategy (Lcom/facebook/ktfmt/format/TrailingCommaManagementStrategy;)Lcom/facebook/ktfmt/format/FormattingOptions$Builder;
165+
}
166+
149167
public final class com/facebook/ktfmt/format/FormattingOptions$Companion {
150168
}
151169

core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,19 @@ data class ParsedArgs(
7575
|Commands options:
7676
| -h, --help Show this help message
7777
| -v, --version Show version
78-
| -n, --dry-run Don't write to files, only report files which
78+
| -n, --dry-run Don't write to files, only report files which
7979
| would have changed
8080
| --meta-style Use 2-space block indenting (default)
8181
| --google-style Google internal style (2 spaces)
8282
| --kotlinlang-style Kotlin language guidelines style (4 spaces)
8383
| --stdin-name=<name> Name to report when formatting code from stdin
84-
| --set-exit-if-changed Sets exit code to 1 if any input file was not
84+
| --set-exit-if-changed Sets exit code to 1 if any input file was not
8585
| formatted/touched
8686
| --do-not-remove-unused-imports Leaves all imports in place, even if not used
8787
| --enable-editorconfig Enable .editorconfig overrides for supported formatting options (limited)
8888
| see https://github.com/facebook/ktfmt/blob/main/README.md
8989
| --quiet Suppress all non-error output
90-
|
90+
|
9191
|ARGFILE:
9292
| If the only argument begins with '@', the remainder of the argument is treated
9393
| as the name of a file to read options and arguments from, one per line.

core/src/main/java/com/facebook/ktfmt/format/FormattingOptions.kt

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ data class FormattingOptions(
5757
/** Whether ktfmt should remove imports that are not used. */
5858
val removeUnusedImports: Boolean = true,
5959

60+
/**
61+
* Whether to preserve user-authored line breaks inside lambda bodies. Enabled by default.
62+
*
63+
* When enabled, a lambda whose body spans multiple lines in the source is kept multi-line. A
64+
* lambda that fits on a single line in the source is left alone. This is general-purpose and
65+
* lets callers preserve structure they care about without ktfmt prescribing a specific outcome.
66+
*
67+
* For example, with this option enabled, the following stays multi-line:
68+
* ```
69+
* App {
70+
* SelectableCard {
71+
* Button { Text("Hello") }
72+
* }
73+
* }
74+
* ```
75+
*
76+
* While this stays single-line:
77+
* ```
78+
* App { SelectableCard { Button { Text("Hello") } } }
79+
* ```
80+
*/
81+
val preserveLambdaBreaks: Boolean = true,
82+
6083
/**
6184
* Print the Ops generated by KotlinInputAstVisitor to help reason about formatting (i.e.,
6285
* newline) decisions
@@ -65,6 +88,8 @@ data class FormattingOptions(
6588
) {
6689
companion object {
6790
const val DEFAULT_MAX_WIDTH: Int = 100
91+
const val DEFAULT_BLOCK_INDENT: Int = 2
92+
const val DEFAULT_CONTINUATION_INDENT: Int = 4
6893
}
6994

7095
@Deprecated("Here just for retrocompatibility reasons. Will be removed on 1.0.0")
@@ -81,11 +106,122 @@ data class FormattingOptions(
81106
continuationIndent = continuationIndent,
82107
trailingCommaManagementStrategy = if (manageTrailingCommas) COMPLETE else NONE,
83108
removeUnusedImports = removeUnusedImports,
109+
// Pinned to false to preserve the behavior callers of this deprecated constructor had before
110+
// preserveLambdaBreaks existed and became the default.
111+
preserveLambdaBreaks = false,
112+
debuggingPrintOpsAfterFormatting = debuggingPrintOpsAfterFormatting,
113+
)
114+
115+
@Deprecated("Here just for retrocompatibility reasons. Will be removed on 1.0.0")
116+
constructor(
117+
maxWidth: Int,
118+
blockIndent: Int,
119+
continuationIndent: Int,
120+
trailingCommaManagementStrategy: TrailingCommaManagementStrategy,
121+
removeUnusedImports: Boolean,
122+
debuggingPrintOpsAfterFormatting: Boolean,
123+
) : this(
124+
maxWidth = maxWidth,
125+
blockIndent = blockIndent,
126+
continuationIndent = continuationIndent,
127+
trailingCommaManagementStrategy = trailingCommaManagementStrategy,
128+
removeUnusedImports = removeUnusedImports,
129+
// Set to false to preserve the behavior callers of this deprecated constructor had before
130+
// preserveLambdaBreaks existed and became the default.
131+
preserveLambdaBreaks = false,
84132
debuggingPrintOpsAfterFormatting = debuggingPrintOpsAfterFormatting,
85133
)
86134

87135
internal val manageTrailingCommas: Boolean
88136
get() = trailingCommaManagementStrategy != NONE
137+
138+
/**
139+
* Returns a [Builder] pre-populated with this instance's values.
140+
*
141+
* Handy for deriving a new [FormattingOptions] from an existing one (such as one of the
142+
* [Formatter] presets) while overriding only a few fields.
143+
*/
144+
fun toBuilder(): Builder =
145+
Builder()
146+
.maxWidth(maxWidth)
147+
.blockIndent(blockIndent)
148+
.continuationIndent(continuationIndent)
149+
.trailingCommaManagementStrategy(trailingCommaManagementStrategy)
150+
.removeUnusedImports(removeUnusedImports)
151+
.preserveLambdaBreaks(preserveLambdaBreaks)
152+
.debuggingPrintOpsAfterFormatting(debuggingPrintOpsAfterFormatting)
153+
154+
/**
155+
* A fluent builder for [FormattingOptions].
156+
*
157+
* Prefer this (or [toBuilder]) over the constructor and generated [copy] when constructing
158+
* options from outside this module. New options can be introduced as additional builder methods
159+
* without breaking source or binary compatibility for existing callers, which is not true of the
160+
* constructor or [copy] overloads.
161+
*
162+
* For example:
163+
* ```
164+
* val options =
165+
* FormattingOptions.Builder()
166+
* .blockIndent(2)
167+
* .continuationIndent(4)
168+
* .preserveLambdaBreaks(true)
169+
* .build()
170+
* ```
171+
*/
172+
class Builder {
173+
private var maxWidth: Int = DEFAULT_MAX_WIDTH
174+
private var blockIndent: Int = DEFAULT_BLOCK_INDENT
175+
private var continuationIndent: Int = DEFAULT_CONTINUATION_INDENT
176+
private var trailingCommaManagementStrategy: TrailingCommaManagementStrategy = COMPLETE
177+
private var removeUnusedImports: Boolean = true
178+
private var preserveLambdaBreaks: Boolean = true
179+
private var debuggingPrintOpsAfterFormatting: Boolean = false
180+
181+
/** @see FormattingOptions.maxWidth */
182+
fun maxWidth(maxWidth: Int): Builder = apply { this.maxWidth = maxWidth }
183+
184+
/** @see FormattingOptions.blockIndent */
185+
fun blockIndent(blockIndent: Int): Builder = apply { this.blockIndent = blockIndent }
186+
187+
/** @see FormattingOptions.continuationIndent */
188+
fun continuationIndent(continuationIndent: Int): Builder = apply {
189+
this.continuationIndent = continuationIndent
190+
}
191+
192+
/** @see FormattingOptions.trailingCommaManagementStrategy */
193+
fun trailingCommaManagementStrategy(
194+
trailingCommaManagementStrategy: TrailingCommaManagementStrategy
195+
): Builder = apply { this.trailingCommaManagementStrategy = trailingCommaManagementStrategy }
196+
197+
/** @see FormattingOptions.removeUnusedImports */
198+
fun removeUnusedImports(removeUnusedImports: Boolean): Builder = apply {
199+
this.removeUnusedImports = removeUnusedImports
200+
}
201+
202+
/** @see FormattingOptions.preserveLambdaBreaks */
203+
fun preserveLambdaBreaks(preserveLambdaBreaks: Boolean): Builder = apply {
204+
this.preserveLambdaBreaks = preserveLambdaBreaks
205+
}
206+
207+
/** @see FormattingOptions.debuggingPrintOpsAfterFormatting */
208+
fun debuggingPrintOpsAfterFormatting(debuggingPrintOpsAfterFormatting: Boolean): Builder =
209+
apply {
210+
this.debuggingPrintOpsAfterFormatting = debuggingPrintOpsAfterFormatting
211+
}
212+
213+
/** Builds a new [FormattingOptions] from the current builder state. */
214+
fun build(): FormattingOptions =
215+
FormattingOptions(
216+
maxWidth = maxWidth,
217+
blockIndent = blockIndent,
218+
continuationIndent = continuationIndent,
219+
trailingCommaManagementStrategy = trailingCommaManagementStrategy,
220+
removeUnusedImports = removeUnusedImports,
221+
preserveLambdaBreaks = preserveLambdaBreaks,
222+
debuggingPrintOpsAfterFormatting = debuggingPrintOpsAfterFormatting,
223+
)
224+
}
89225
}
90226

91227
enum class TrailingCommaManagementStrategy(

core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,8 +930,13 @@ class KotlinInputAstVisitor(
930930
builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent)
931931
builder.block(bracePlusBlockIndent) {
932932
builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO)
933+
934+
val shouldForceMultiline =
935+
options.preserveLambdaBreaks && hasSourceNewlineInLambdaBody(lambdaExpression)
936+
933937
if (
934-
expressionStatements.size == 1 &&
938+
!shouldForceMultiline &&
939+
expressionStatements.size == 1 &&
935940
expressionStatements.first() !is KtReturnExpression &&
936941
!bodyExpression.startsWithComment()
937942
) {
@@ -1605,6 +1610,20 @@ class KotlinInputAstVisitor(
16051610
return false
16061611
}
16071612

1613+
/**
1614+
* Returns true if the source code contains a newline anywhere inside the body of
1615+
* [lambdaExpression] — that is, between the opening `{` and the closing `}` of the function
1616+
* literal. Used by [FormattingOptions.preserveLambdaBreaks] to keep user-authored multi-line
1617+
* lambdas multi-line.
1618+
*/
1619+
private fun hasSourceNewlineInLambdaBody(lambdaExpression: KtLambdaExpression): Boolean {
1620+
val functionLiteral = lambdaExpression.functionLiteral
1621+
for (child in functionLiteral.node.children()) {
1622+
if (child.psi is PsiWhiteSpace && child.text.contains('\n')) return true
1623+
}
1624+
return false
1625+
}
1626+
16081627
/** See [isLambdaOrScopingFunction] for examples. */
16091628
private fun visitLambdaOrScopingFunction(expr: PsiElement?) {
16101629
val breakToExpr = genSym()

0 commit comments

Comments
 (0)