Skip to content

Commit 9d247cf

Browse files
committed
Refactor selection support in response to feedback (for <#297>).
Make `Selection` and enum so that we don't need to use optionals everywhere. And use `Range<AbsolutePosition>` rather than a custom type (matching recent refactorings in swift-syntax).
1 parent b9eda3a commit 9d247cf

File tree

11 files changed

+88
-94
lines changed

11 files changed

+88
-94
lines changed

Sources/SwiftFormat/API/Selection.swift

Lines changed: 41 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,72 +14,63 @@ import Foundation
1414
import SwiftSyntax
1515

1616
/// The selection as given on the command line - an array of offets and lengths
17-
public struct Selection {
18-
// TODO: make this a typealias instead
19-
public struct Range: Decodable {
20-
public let offset: AbsolutePosition
21-
public let length: Int
22-
public var end: AbsolutePosition { offset.advanced(by: length) }
17+
public enum Selection {
18+
case infinite
19+
case ranges([Range<AbsolutePosition>])
2320

24-
public init(from decoder: any Decoder) throws {
25-
let container = try decoder.container(keyedBy: CodingKeys.self)
26-
offset = AbsolutePosition(utf8Offset: try container.decode(Int.self, forKey: .offset))
27-
length = try container.decode(Int.self, forKey: .length)
28-
}
29-
30-
public init(start: AbsolutePosition, end: AbsolutePosition) {
31-
offset = start
32-
length = end.utf8Offset - start.utf8Offset
33-
}
34-
35-
public init(start: Int, end: Int) {
36-
offset = AbsolutePosition(utf8Offset: start)
37-
length = end - start
38-
}
39-
40-
enum CodingKeys: CodingKey {
41-
case offset
42-
case length
21+
public init(json: String) {
22+
if let data = json.data(using: .utf8),
23+
let ranges = try? JSONDecoder().decode([Range<AbsolutePosition>].self, from: data) {
24+
self = .ranges(ranges)
25+
} else {
26+
self = .infinite
4327
}
4428
}
4529

46-
public let ranges: [Range]
47-
48-
public init?(json: String) {
49-
guard let data = json.data(using: .utf8),
50-
let ranges = try? JSONDecoder().decode([Range].self, from: data) else {
51-
return nil
30+
public init(ranges: [Range<AbsolutePosition>]) {
31+
if ranges.isEmpty {
32+
self = .infinite
33+
} else {
34+
self = .ranges(ranges)
5235
}
53-
self.ranges = ranges
54-
}
55-
56-
public init(ranges: [Selection.Range]) {
57-
self.ranges = ranges
5836
}
5937

6038
public func contains(_ position: AbsolutePosition) -> Bool {
61-
return ranges.contains { return $0.offset <= position && position < $0.end }
39+
switch self {
40+
case .infinite:
41+
return true
42+
case .ranges(let ranges):
43+
return ranges.contains { $0.contains(position) }
44+
}
6245
}
6346

64-
public func overlaps(_ range: Range) -> Bool {
65-
return ranges.contains {
66-
let maxStart = max($0.offset, range.offset)
67-
let minEnd = (min($0.end, range.end))
68-
return maxStart <= minEnd
47+
public func overlapsOrTouches(_ range: Range<AbsolutePosition>) -> Bool {
48+
switch self {
49+
case .infinite:
50+
return true
51+
case .ranges(let ranges):
52+
return ranges.contains { $0.overlapsOrTouches(range) }
6953
}
7054
}
7155
}
7256

7357

7458
public extension Syntax {
75-
/// return true if the node is completely inside any range in the selection (or if there's
76-
/// no selection)
77-
func isInsideSelection(_ selection: Selection?) -> Bool {
78-
// no selection means we should process everything
79-
guard let selection else { return true }
80-
81-
return selection.ranges.contains { range in
82-
return range.offset <= position && endPosition <= range.end
59+
/// return true if the node is _completely_ inside any range in the selection
60+
func isInsideSelection(_ selection: Selection) -> Bool {
61+
switch selection {
62+
case .infinite:
63+
return true
64+
case .ranges(let ranges):
65+
return ranges.contains { return $0.lowerBound <= position && endPosition <= $0.upperBound }
8366
}
8467
}
8568
}
69+
70+
// FIXME: temporary until we convert argument parsing
71+
extension AbsolutePosition : Decodable {
72+
public init(from decoder: any Decoder) throws {
73+
var container = try decoder.unkeyedContainer()
74+
self.init(utf8Offset: try container.decode(Int.self))
75+
}
76+
}

Sources/SwiftFormat/API/SwiftFormatter.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public final class SwiftFormatter {
2222
public let configuration: Configuration
2323

2424
/// the ranges of text to format
25-
public var selection: Selection? = nil
25+
public var selection: Selection = .infinite
2626

2727
/// An optional callback that will be notified with any findings encountered during formatting.
2828
public let findingConsumer: ((Finding) -> Void)?
@@ -73,7 +73,7 @@ public final class SwiftFormatter {
7373
try format(
7474
source: String(contentsOf: url, encoding: .utf8),
7575
assumingFileURL: url,
76-
selection: nil,
76+
selection: .infinite,
7777
to: &outputStream,
7878
parsingDiagnosticHandler: parsingDiagnosticHandler)
7979
}
@@ -99,7 +99,7 @@ public final class SwiftFormatter {
9999
public func format<Output: TextOutputStream>(
100100
source: String,
101101
assumingFileURL url: URL?,
102-
selection: Selection?,
102+
selection: Selection,
103103
to outputStream: inout Output,
104104
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
105105
) throws {
@@ -141,7 +141,7 @@ public final class SwiftFormatter {
141141
/// - Throws: If an unrecoverable error occurs when formatting the code.
142142
public func format<Output: TextOutputStream>(
143143
syntax: SourceFileSyntax, source: String, operatorTable: OperatorTable,
144-
assumingFileURL url: URL?, selection: Selection?, to outputStream: inout Output
144+
assumingFileURL url: URL?, selection: Selection, to outputStream: inout Output
145145
) throws {
146146
let assumedURL = url ?? URL(fileURLWithPath: "source")
147147
let context = Context(

Sources/SwiftFormat/Core/Context.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public final class Context {
4040
let configuration: Configuration
4141

4242
/// The optional ranges to process
43-
let selection: Selection?
43+
let selection: Selection
4444

4545
/// Defines the operators and their precedence relationships that were used during parsing.
4646
let operatorTable: OperatorTable
@@ -69,7 +69,7 @@ public final class Context {
6969
operatorTable: OperatorTable,
7070
findingConsumer: ((Finding) -> Void)?,
7171
fileURL: URL,
72-
selection: Selection? = nil,
72+
selection: Selection = .infinite,
7373
sourceFileSyntax: SourceFileSyntax,
7474
source: String? = nil,
7575
ruleNameCache: [ObjectIdentifier: String]

Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3434
private let config: Configuration
3535
private let operatorTable: OperatorTable
3636
private let maxlinelength: Int
37-
private let selection: Selection?
37+
private let selection: Selection
3838

3939
/// The index of the most recently appended break, or nil when no break has been appended.
4040
private var lastBreakIndex: Int? = nil
@@ -72,7 +72,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
7272
/// Tracks whether we last considered ourselves inside the selection
7373
private var isInsideSelection = true
7474

75-
init(configuration: Configuration, selection: Selection?, operatorTable: OperatorTable) {
75+
init(configuration: Configuration, selection: Selection, operatorTable: OperatorTable) {
7676
self.config = configuration
7777
self.selection = selection
7878
self.operatorTable = operatorTable
@@ -82,7 +82,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
8282

8383
func makeStream(from node: Syntax) -> [Token] {
8484
// if we have a selection, then we start outside of it
85-
if selection != nil {
85+
if case .ranges = selection {
8686
appendToken(.disableFormatting(AbsolutePosition(utf8Offset: 0)))
8787
isInsideSelection = false
8888
}
@@ -92,7 +92,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
9292
self.walk(node)
9393

9494
// Make sure we output any trailing text after the last selection range
95-
if selection != nil {
95+
if case .ranges = selection {
9696
appendToken(.enableFormatting(nil))
9797
}
9898
defer { tokens = [] }
@@ -2735,9 +2735,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
27352735
extractLeadingTrivia(token)
27362736
closeScopeTokens.forEach(appendToken)
27372737

2738-
generateEnable(Selection.Range(
2739-
start: token.positionAfterSkippingLeadingTrivia,
2740-
end: token.endPositionBeforeTrailingTrivia))
2738+
generateEnable(
2739+
token.positionAfterSkippingLeadingTrivia ..< token.endPositionBeforeTrailingTrivia
2740+
)
27412741

27422742
if !ignoredTokens.contains(token) {
27432743
// Otherwise, it's just a regular token, so add the text as-is.
@@ -2753,16 +2753,16 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
27532753
return .skipChildren
27542754
}
27552755

2756-
func generateEnable(_ range: Selection.Range) {
2757-
guard let selection else { return }
2758-
if !isInsideSelection && selection.overlaps(range) {
2759-
appendToken(.enableFormatting(range.offset))
2756+
func generateEnable(_ range: Range<AbsolutePosition>) {
2757+
if case .infinite = selection { return }
2758+
if !isInsideSelection && selection.overlapsOrTouches(range) {
2759+
appendToken(.enableFormatting(range.lowerBound))
27602760
isInsideSelection = true
27612761
}
27622762
}
27632763

27642764
func generateDisable(_ position: AbsolutePosition) {
2765-
guard let selection else { return }
2765+
if case .infinite = selection { return }
27662766
if isInsideSelection && !selection.contains(position) {
27672767
appendToken(.disableFormatting(position))
27682768
isInsideSelection = false
@@ -3270,7 +3270,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32703270
switch piece {
32713271
case .lineComment(let text):
32723272
if index > 0 || isStartOfFile {
3273-
generateEnable(Selection.Range(start: position, end: position + piece.sourceLength))
3273+
generateEnable(position ..< position + piece.sourceLength)
32743274
appendToken(.comment(Comment(kind: .line, text: text), wasEndOfLine: false))
32753275
generateDisable(position + piece.sourceLength)
32763276
appendNewlines(.soft)
@@ -3280,7 +3280,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32803280

32813281
case .blockComment(let text):
32823282
if index > 0 || isStartOfFile {
3283-
generateEnable(Selection.Range(start: position, end: position + piece.sourceLength))
3283+
generateEnable(position ..< position + piece.sourceLength)
32843284
appendToken(.comment(Comment(kind: .block, text: text), wasEndOfLine: false))
32853285
generateDisable(position + piece.sourceLength)
32863286
// There is always a break after the comment to allow a discretionary newline after it.
@@ -3297,15 +3297,15 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32973297
requiresNextNewline = false
32983298

32993299
case .docLineComment(let text):
3300-
generateEnable(Selection.Range(start: position, end: position + piece.sourceLength))
3300+
generateEnable(position ..< position + piece.sourceLength)
33013301
appendToken(.comment(Comment(kind: .docLine, text: text), wasEndOfLine: false))
33023302
generateDisable(position + piece.sourceLength)
33033303
appendNewlines(.soft)
33043304
isStartOfFile = false
33053305
requiresNextNewline = true
33063306

33073307
case .docBlockComment(let text):
3308-
generateEnable(Selection.Range(start: position, end: position + piece.sourceLength))
3308+
generateEnable(position ..< position + piece.sourceLength)
33093309
appendToken(.comment(Comment(kind: .docBlock, text: text), wasEndOfLine: false))
33103310
generateDisable(position + piece.sourceLength)
33113311
appendNewlines(.soft)
@@ -4049,7 +4049,7 @@ extension Syntax {
40494049
/// Creates a pretty-printable token stream for the provided Syntax node.
40504050
func makeTokenStream(
40514051
configuration: Configuration,
4052-
selection: Selection?,
4052+
selection: Selection,
40534053
operatorTable: OperatorTable
40544054
) -> [Token] {
40554055
let commentsMoved = CommentMovingRewriter(selection: selection).rewrite(self)
@@ -4067,11 +4067,11 @@ extension Syntax {
40674067
/// For example, comments after binary operators are relocated to be before the operator, which
40684068
/// results in fewer line breaks with the comment closer to the relevant tokens.
40694069
class CommentMovingRewriter: SyntaxRewriter {
4070-
init(selection: Selection? = nil) {
4070+
init(selection: Selection = .infinite) {
40714071
self.selection = selection
40724072
}
40734073

4074-
var selection: Selection?
4074+
var selection: Selection
40754075

40764076
override func visit(_ node: SourceFileSyntax) -> SourceFileSyntax {
40774077
if shouldFormatterIgnore(file: node) {

Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ open class DiagnosingTestCase: XCTestCase {
1515
public func makeContext(
1616
sourceFileSyntax: SourceFileSyntax,
1717
configuration: Configuration? = nil,
18-
selection: Selection?,
18+
selection: Selection,
1919
findingConsumer: @escaping (Finding) -> Void
2020
) -> Context {
2121
let context = Context(

Sources/_SwiftFormatTestSupport/MarkedText.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,23 @@ public struct MarkedText {
2222
public let textWithoutMarkers: String
2323

2424
/// If the marked text contains "⏩" and "⏪", they're used to create a selection
25-
public var selection: Selection?
25+
public var selection: Selection
2626

2727
/// Creates a new `MarkedText` value by extracting emoji markers from the given text.
2828
public init(textWithMarkers markedText: String) {
2929
var text = ""
3030
var markers = [String: Int]()
3131
var lastIndex = markedText.startIndex
32-
var ranges = [Selection.Range]()
33-
var lastRangeStart = 0
32+
var ranges = [Range<AbsolutePosition>]()
33+
var lastRangeStart = AbsolutePosition(utf8Offset: 0)
3434
for marker in findMarkedRanges(in: markedText) {
3535
text += markedText[lastIndex..<marker.range.lowerBound]
3636
lastIndex = marker.range.upperBound
3737

3838
if marker.name == "" {
39-
lastRangeStart = text.utf8.count
39+
lastRangeStart = AbsolutePosition(utf8Offset: text.utf8.count)
4040
} else if marker.name == "" {
41-
ranges.append(Selection.Range(start: lastRangeStart, end: text.utf8.count))
41+
ranges.append(lastRangeStart ..< AbsolutePosition(utf8Offset: text.utf8.count))
4242
} else {
4343
assert(markers[marker.name] == nil, "Marker names must be unique")
4444
markers[marker.name] = text.utf8.count
@@ -49,9 +49,7 @@ public struct MarkedText {
4949

5050
self.markers = markers
5151
self.textWithoutMarkers = text
52-
if !ranges.isEmpty {
53-
selection = Selection(ranges: ranges)
54-
}
52+
self.selection = Selection(ranges: ranges)
5553
}
5654
}
5755

Sources/swift-format/Frontend/Frontend.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class Frontend {
3636
let configuration: Configuration
3737

3838
/// the selected ranges to process
39-
let selection: Selection?
39+
let selection: Selection
4040

4141
/// Returns the string contents of the file.
4242
///
@@ -48,7 +48,12 @@ class Frontend {
4848
return String(data: sourceData, encoding: .utf8)
4949
}()
5050

51-
init(fileHandle: FileHandle, url: URL, configuration: Configuration, selection: Selection? = nil) {
51+
init(
52+
fileHandle: FileHandle,
53+
url: URL,
54+
configuration: Configuration,
55+
selection: Selection = .infinite
56+
) {
5257
self.fileHandle = fileHandle
5358
self.url = url
5459
self.configuration = configuration
@@ -120,7 +125,7 @@ class Frontend {
120125
return
121126
}
122127

123-
var selection: Selection?
128+
var selection: Selection = .infinite
124129
if let selectionString = lintFormatOptions.selection {
125130
// TODO: do this when parsing args to catch errors?
126131
selection = Selection(json: selectionString)
@@ -173,7 +178,7 @@ class Frontend {
173178
return nil
174179
}
175180

176-
var selection: Selection?
181+
var selection: Selection = .infinite
177182
if let selectionString = lintFormatOptions.selection {
178183
// TODO: do this when parsing args to catch errors?
179184
selection = Selection(json: selectionString)

Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ final class WhitespaceLinterPerformanceTests: DiagnosingTestCase {
5858
/// - expected: The formatted text.
5959
private func performWhitespaceLint(input: String, expected: String) {
6060
let sourceFileSyntax = Parser.parse(source: input)
61-
let context = makeContext(sourceFileSyntax: sourceFileSyntax, selection: nil,
61+
let context = makeContext(sourceFileSyntax: sourceFileSyntax, selection: .infinite,
6262
findingConsumer: { _ in })
6363
let linter = WhitespaceLinter(user: input, formatted: expected, context: context)
6464
linter.lint()

0 commit comments

Comments
 (0)