Skip to content

Commit a47ad53

Browse files
authored
Merge pull request swiftlang#128 from dylansturg/contextual_breaking_tokens
Support conditional indentation for dot-chained expressions to align dots
2 parents 63366a8 + 329a53d commit a47ad53

File tree

8 files changed

+678
-21
lines changed

8 files changed

+678
-21
lines changed

Documentation/Configuration.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ top-level keys and values:
8080
conditional compilation blocks are indented. If this setting is `false` the body
8181
of `#if`, `#elseif`, and `#else` is not indented. Defaults to `true`.
8282

83+
* `lineBreakAroundMultilineExpressionChainComponents` _(boolean)_: Determines whether
84+
line breaks should be forced before and after multiline components of dot-chained
85+
expressions, such as function calls and subscripts chained together through member
86+
access (i.e. "." expressions). When any component is multiline and this option is
87+
true, a line break is forced before the "." of the component and after the component's
88+
closing delimiter (i.e. right paren, right bracket, right brace, etc.).
89+
8390
> TODO: Add support for enabling/disabling specific syntax transformations in
8491
> the pipeline.
8592

Sources/SwiftFormatConfiguration/Configuration.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public struct Configuration: Codable, Equatable {
3232
case lineBreakBeforeEachGenericRequirement
3333
case prioritizeKeepingFunctionOutputTogether
3434
case indentConditionalCompilationBlocks
35+
case lineBreakAroundMultilineExpressionChainComponents
3536
case rules
3637
}
3738

@@ -111,6 +112,13 @@ public struct Configuration: Codable, Equatable {
111112
/// Determines the indentation behavior for `#if`, `#elseif`, and `#else`.
112113
public var indentConditionalCompilationBlocks = true
113114

115+
/// Determines whether line breaks should be forced before and after multiline components of
116+
/// dot-chained expressions, such as function calls and subscripts chained together through member
117+
/// access (i.e. "." expressions). When any component is multiline and this option is true, a line
118+
/// break is forced before the "." of the component and after the component's closing delimiter
119+
/// (i.e. right paren, right bracket, right brace, etc.).
120+
public var lineBreakAroundMultilineExpressionChainComponents = false
121+
114122
/// Constructs a Configuration with all default values.
115123
public init() {
116124
self.version = highestSupportedConfigurationVersion
@@ -157,6 +165,9 @@ public struct Configuration: Codable, Equatable {
157165
= try container.decodeIfPresent(Bool.self, forKey: .prioritizeKeepingFunctionOutputTogether) ?? false
158166
self.indentConditionalCompilationBlocks
159167
= try container.decodeIfPresent(Bool.self, forKey: .indentConditionalCompilationBlocks) ?? true
168+
self.lineBreakAroundMultilineExpressionChainComponents =
169+
try container.decodeIfPresent(
170+
Bool.self, forKey: .lineBreakAroundMultilineExpressionChainComponents) ?? false
160171

161172
// If the `rules` key is not present at all, default it to the built-in set
162173
// so that the behavior is the same as if the configuration had been
@@ -181,6 +192,9 @@ public struct Configuration: Codable, Equatable {
181192
try container.encode(lineBreakBeforeEachGenericRequirement, forKey: .lineBreakBeforeEachGenericRequirement)
182193
try container.encode(prioritizeKeepingFunctionOutputTogether, forKey: .prioritizeKeepingFunctionOutputTogether)
183194
try container.encode(indentConditionalCompilationBlocks, forKey: .indentConditionalCompilationBlocks)
195+
try container.encode(
196+
lineBreakAroundMultilineExpressionChainComponents,
197+
forKey: .lineBreakAroundMultilineExpressionChainComponents)
184198
try container.encode(rules, forKey: .rules)
185199
}
186200
}

Sources/SwiftFormatPrettyPrint/PrettyPrint.swift

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,25 @@ public class PrettyPrinter {
4444
var contributesBlockIndent: Bool
4545
}
4646

47+
/// Records state of `contextualBreakingStart` tokens.
48+
private struct ActiveBreakingContext {
49+
/// The line number in the `outputBuffer` where a start token appeared.
50+
let lineNumber: Int
51+
52+
enum BreakingBehavior {
53+
/// The behavior hasn't been determined. This is treated as `continuation`.
54+
case unset
55+
/// The break is created as a `continuation` break, setting `currentLineIsContinuation` when
56+
/// it fires.
57+
case continuation
58+
/// The break maintains the existing value of `currentLineIsContinuation` when it fires.
59+
case maintain
60+
}
61+
62+
/// The behavior to use when a `contextual` break fires inside of this break context.
63+
var contextualBreakingBehavior = BreakingBehavior.unset
64+
}
65+
4766
private let context: Context
4867
private var configuration: Configuration { return context.configuration }
4968
private let maxLineLength: Int
@@ -75,6 +94,12 @@ public class PrettyPrinter {
7594
/// so far.
7695
private var activeOpenBreaks: [ActiveOpenBreak] = []
7796

97+
/// Stack of the active breaking contexts.
98+
private var activeBreakingContexts: [ActiveBreakingContext] = []
99+
100+
/// The most recently ended breaking context, used to force certain following `contextual` breaks.
101+
private var lastEndedBreakingContext: ActiveBreakingContext? = nil
102+
78103
/// Keeps track of the current line number being printed.
79104
private var lineNumber: Int = 1
80105

@@ -243,6 +268,24 @@ public class PrettyPrinter {
243268
assert(length >= 0, "Token lengths must be positive")
244269

245270
switch token {
271+
case .contextualBreakingStart:
272+
activeBreakingContexts.append(ActiveBreakingContext(lineNumber: lineNumber))
273+
274+
// Discard the last finished breaking context to keep it from effecting breaks inside of the
275+
// new context. The discarded context has already either had an impact on the contextual break
276+
// after it or there was no relevant contextual break, so it's safe to discard.
277+
lastEndedBreakingContext = nil
278+
279+
case .contextualBreakingEnd:
280+
guard let closedContext = activeBreakingContexts.popLast() else {
281+
fatalError("Encountered unmatched contextualBreakingEnd token.")
282+
}
283+
284+
// Break contexts create scopes, and a breaking context should never be carried between
285+
// scopes. When there's no active break context, discard the popped one to prevent carrying it
286+
// into a new scope.
287+
lastEndedBreakingContext = activeBreakingContexts.isEmpty ? nil : closedContext
288+
246289
// Check if we need to force breaks in this group, and calculate the indentation to be used in
247290
// the group.
248291
case .open(let breaktype):
@@ -384,6 +427,42 @@ public class PrettyPrinter {
384427

385428
case .reset:
386429
mustBreak = currentLineIsContinuation
430+
431+
case .contextual:
432+
// When the last context spanned multiple lines, move the next context (in the same parent
433+
// break context scope) onto its own line. For example, this is used when the previous
434+
// context includes a multiline trailing closure or multiline function argument list.
435+
if let lastBreakingContext = lastEndedBreakingContext {
436+
if configuration.lineBreakAroundMultilineExpressionChainComponents {
437+
mustBreak = lastBreakingContext.lineNumber != lineNumber
438+
}
439+
}
440+
441+
// Wait for a contextual break to fire and then update the breaking behavior for the rest of
442+
// the contextual breaks in this scope to match the behavior of the one that fired.
443+
let willFire = (!isAtStartOfLine && length > spaceRemaining) || mustBreak
444+
if willFire {
445+
// Update the active breaking context according to the most recently finished breaking
446+
// context so all following contextual breaks in this scope to have matching behavior.
447+
if let closedContext = lastEndedBreakingContext,
448+
let activeContext = activeBreakingContexts.last,
449+
case .unset = activeContext.contextualBreakingBehavior
450+
{
451+
activeBreakingContexts[activeBreakingContexts.count - 1].contextualBreakingBehavior =
452+
(closedContext.lineNumber == lineNumber) ? .continuation : .maintain
453+
}
454+
}
455+
456+
if let activeBreakingContext = activeBreakingContexts.last {
457+
switch activeBreakingContext.contextualBreakingBehavior {
458+
case .unset, .continuation:
459+
isContinuationIfBreakFires = true
460+
case .maintain:
461+
isContinuationIfBreakFires = currentLineIsContinuation
462+
}
463+
}
464+
465+
lastEndedBreakingContext = nil
387466
}
388467

389468
var overrideBreakingSuppressed = false
@@ -494,6 +573,12 @@ public class PrettyPrinter {
494573
// Calculate token lengths
495574
for (i, token) in tokens.enumerated() {
496575
switch token {
576+
case .contextualBreakingStart:
577+
lengths.append(0)
578+
579+
case .contextualBreakingEnd:
580+
lengths.append(0)
581+
497582
// Open tokens have lengths equal to the total of the contents of its group. The value is
498583
// calcualted when close tokens are encountered.
499584
case .open:
@@ -667,9 +752,17 @@ public class PrettyPrinter {
667752
printDebugIndent()
668753
print("[COMMA DELIMITED START Idx: \(idx)]")
669754

670-
case .commaDelimitedRegionEnd:
671-
printDebugIndent()
672-
print("[COMMA DELIMITED END Idx: \(idx)]")
755+
case .commaDelimitedRegionEnd:
756+
printDebugIndent()
757+
print("[COMMA DELIMITED END Idx: \(idx)]")
758+
759+
case .contextualBreakingStart:
760+
printDebugIndent()
761+
print("[START BREAKING CONTEXT Idx: \(idx)]")
762+
763+
case .contextualBreakingEnd:
764+
printDebugIndent()
765+
print("[END BREAKING CONTEXT Idx: \(idx)]")
673766
}
674767
}
675768

Sources/SwiftFormatPrettyPrint/Token.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ enum BreakKind: Equatable {
111111
/// ```
112112
case reset
113113

114+
/// A `contextual` break acts as either a `continue` break or maintains the existing level of
115+
/// indentation when it fires. The contextual breaking beahvior of a given contextual breaking
116+
/// scope (i.e. inside a `contextualBreakingStart`/`contextualBreakingEnd` region) is set by the
117+
/// first child `contextualBreakingStart`/`contextualBreakingEnd` pair. When the first child is
118+
/// multiline the contextual breaks maintain indentation and they are continuations otherwise.
119+
///
120+
/// These are used when multiple related breaks need to exhibit the same behavior based the
121+
/// context in which they appear. For example, when breaks exist between expressions that are
122+
/// chained together (e.g. member access) and indenting the line *after* a closing paren/brace
123+
/// looks better indented when the previous expr was 1 line but not indented when the expr was
124+
/// multiline.
125+
case contextual
126+
114127
/// A `close` break that defaults to forced breaking behavior.
115128
static let close = BreakKind.close(mustBreak: true)
116129

@@ -179,6 +192,12 @@ enum Token {
179192
/// if and only if the collection spans multiple lines.
180193
case commaDelimitedRegionEnd(hasTrailingComma: Bool)
181194

195+
/// Starts a scope where `contextual` breaks have consistent behavior.
196+
case contextualBreakingStart
197+
198+
/// Ends a scope where `contextual` breaks have consistent behavior.
199+
case contextualBreakingEnd
200+
182201
// Convenience overloads for the enum types
183202
static let open = Token.open(.inconsistent, 0)
184203

0 commit comments

Comments
 (0)