Skip to content

Commit e5360f4

Browse files
allevatoahoppen
authored andcommitted
Merge pull request swiftlang#708 from apple/dewing/FormatRanges
Support for formatting a selection
1 parent babff46 commit e5360f4

22 files changed

+743
-60
lines changed
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import SwiftSyntax
15+
16+
/// The selection as given on the command line - an array of offets and lengths
17+
public enum Selection {
18+
case infinite
19+
case ranges([Range<AbsolutePosition>])
20+
21+
/// Create a selection from an array of utf8 ranges. An empty array means an infinite selection.
22+
public init(offsetRanges: [Range<Int>]) {
23+
if offsetRanges.isEmpty {
24+
self = .infinite
25+
} else {
26+
let ranges = offsetRanges.map {
27+
AbsolutePosition(utf8Offset: $0.lowerBound) ..< AbsolutePosition(utf8Offset: $0.upperBound)
28+
}
29+
self = .ranges(ranges)
30+
}
31+
}
32+
33+
public func contains(_ position: AbsolutePosition) -> Bool {
34+
switch self {
35+
case .infinite:
36+
return true
37+
case .ranges(let ranges):
38+
return ranges.contains { $0.contains(position) }
39+
}
40+
}
41+
42+
public func overlapsOrTouches(_ range: Range<AbsolutePosition>) -> Bool {
43+
switch self {
44+
case .infinite:
45+
return true
46+
case .ranges(let ranges):
47+
return ranges.contains { $0.overlapsOrTouches(range) }
48+
}
49+
}
50+
}
51+
52+
53+
public extension Syntax {
54+
/// - Returns: `true` if the node is _completely_ inside any range in the selection
55+
func isInsideSelection(_ selection: Selection) -> Bool {
56+
switch selection {
57+
case .infinite:
58+
return true
59+
case .ranges(let ranges):
60+
return ranges.contains { return $0.lowerBound <= position && endPosition <= $0.upperBound }
61+
}
62+
}
63+
}

Sources/SwiftFormat/API/SwiftFormatter.swift

+13-15
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -70,6 +70,7 @@ public final class SwiftFormatter {
7070
try format(
7171
source: String(contentsOf: url, encoding: .utf8),
7272
assumingFileURL: url,
73+
selection: .infinite,
7374
to: &outputStream,
7475
parsingDiagnosticHandler: parsingDiagnosticHandler)
7576
}
@@ -86,6 +87,7 @@ public final class SwiftFormatter {
8687
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree,
8788
/// which is associated with any diagnostics emitted during formatting. If this is nil, a
8889
/// dummy value will be used.
90+
/// - selection: The ranges to format
8991
/// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will
9092
/// be written.
9193
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
@@ -94,6 +96,7 @@ public final class SwiftFormatter {
9496
public func format<Output: TextOutputStream>(
9597
source: String,
9698
assumingFileURL url: URL?,
99+
selection: Selection,
97100
to outputStream: inout Output,
98101
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
99102
) throws {
@@ -108,8 +111,8 @@ public final class SwiftFormatter {
108111
assumingFileURL: url,
109112
parsingDiagnosticHandler: parsingDiagnosticHandler)
110113
try format(
111-
syntax: sourceFile, operatorTable: .standardOperators, assumingFileURL: url, source: source,
112-
to: &outputStream)
114+
syntax: sourceFile, source: source, operatorTable: .standardOperators, assumingFileURL: url,
115+
selection: selection, to: &outputStream)
113116
}
114117

115118
/// Formats the given Swift syntax tree and writes the result to an output stream.
@@ -122,32 +125,26 @@ public final class SwiftFormatter {
122125
///
123126
/// - Parameters:
124127
/// - syntax: The Swift syntax tree to be converted to source code and formatted.
128+
/// - source: The original Swift source code used to build the syntax tree.
125129
/// - operatorTable: The table that defines the operators and their precedence relationships.
126130
/// This must be the same operator table that was used to fold the expressions in the `syntax`
127131
/// argument.
128132
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree,
129133
/// which is associated with any diagnostics emitted during formatting. If this is nil, a
130134
/// dummy value will be used.
135+
/// - selection: The ranges to format
131136
/// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will
132137
/// be written.
133138
/// - Throws: If an unrecoverable error occurs when formatting the code.
134139
public func format<Output: TextOutputStream>(
135-
syntax: SourceFileSyntax, operatorTable: OperatorTable, assumingFileURL url: URL?,
136-
to outputStream: inout Output
137-
) throws {
138-
try format(
139-
syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: nil,
140-
to: &outputStream)
141-
}
142-
143-
private func format<Output: TextOutputStream>(
144-
syntax: SourceFileSyntax, operatorTable: OperatorTable,
145-
assumingFileURL url: URL?, source: String?, to outputStream: inout Output
140+
syntax: SourceFileSyntax, source: String, operatorTable: OperatorTable,
141+
assumingFileURL url: URL?, selection: Selection, to outputStream: inout Output
146142
) throws {
147143
let assumedURL = url ?? URL(fileURLWithPath: "source")
148144
let context = Context(
149145
configuration: configuration, operatorTable: operatorTable, findingConsumer: findingConsumer,
150-
fileURL: assumedURL, sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache)
146+
fileURL: assumedURL, selection: selection, sourceFileSyntax: syntax, source: source,
147+
ruleNameCache: ruleNameCache)
151148
let pipeline = FormatPipeline(context: context)
152149
let transformedSyntax = pipeline.rewrite(Syntax(syntax))
153150

@@ -158,6 +155,7 @@ public final class SwiftFormatter {
158155

159156
let printer = PrettyPrinter(
160157
context: context,
158+
source: source,
161159
node: transformedSyntax,
162160
printTokenStream: debugOptions.contains(.dumpTokenStream),
163161
whitespaceOnly: false)

Sources/SwiftFormat/API/SwiftLinter.swift

+4-2
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,18 @@ public final class SwiftLinter {
119119
/// - Throws: If an unrecoverable error occurs when formatting the code.
120120
public func lint(
121121
syntax: SourceFileSyntax,
122+
source: String,
122123
operatorTable: OperatorTable,
123124
assumingFileURL url: URL
124125
) throws {
125-
try lint(syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: nil)
126+
try lint(syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: source)
126127
}
127128

128129
private func lint(
129130
syntax: SourceFileSyntax,
130131
operatorTable: OperatorTable,
131132
assumingFileURL url: URL,
132-
source: String?
133+
source: String
133134
) throws {
134135
let context = Context(
135136
configuration: configuration, operatorTable: operatorTable, findingConsumer: findingConsumer,
@@ -145,6 +146,7 @@ public final class SwiftLinter {
145146
// pretty-printer.
146147
let printer = PrettyPrinter(
147148
context: context,
149+
source: source,
148150
node: Syntax(syntax),
149151
printTokenStream: debugOptions.contains(.dumpTokenStream),
150152
whitespaceOnly: true)

Sources/SwiftFormat/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ add_library(SwiftFormat
1414
API/Finding.swift
1515
API/FindingCategorizing.swift
1616
API/Indent.swift
17+
API/Selection.swift
1718
API/SwiftFormatError.swift
1819
API/SwiftFormatter.swift
1920
API/SwiftLinter.swift

Sources/SwiftFormat/Core/Context.swift

+10-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -39,6 +39,9 @@ public final class Context {
3939
/// The configuration for this run of the pipeline, provided by a configuration JSON file.
4040
let configuration: Configuration
4141

42+
/// The selection to process
43+
let selection: Selection
44+
4245
/// Defines the operators and their precedence relationships that were used during parsing.
4346
let operatorTable: OperatorTable
4447

@@ -66,6 +69,7 @@ public final class Context {
6669
operatorTable: OperatorTable,
6770
findingConsumer: ((Finding) -> Void)?,
6871
fileURL: URL,
72+
selection: Selection = .infinite,
6973
sourceFileSyntax: SourceFileSyntax,
7074
source: String? = nil,
7175
ruleNameCache: [ObjectIdentifier: String]
@@ -74,6 +78,7 @@ public final class Context {
7478
self.operatorTable = operatorTable
7579
self.findingEmitter = FindingEmitter(consumer: findingConsumer)
7680
self.fileURL = fileURL
81+
self.selection = selection
7782
self.importsXCTest = .notDetermined
7883
let tree = source.map { Parser.parse(source: $0) } ?? sourceFileSyntax
7984
self.sourceLocationConverter =
@@ -86,8 +91,10 @@ public final class Context {
8691
}
8792

8893
/// Given a rule's name and the node it is examining, determine if the rule is disabled at this
89-
/// location or not.
90-
func isRuleEnabled<R: Rule>(_ rule: R.Type, node: Syntax) -> Bool {
94+
/// location or not. Also makes sure the entire node is contained inside any selection.
95+
func shouldFormat<R: Rule>(_ rule: R.Type, node: Syntax) -> Bool {
96+
guard node.isInsideSelection(selection) else { return false }
97+
9198
let loc = node.startLocation(converter: self.sourceLocationConverter)
9299

93100
assert(

Sources/SwiftFormat/Core/LintPipeline.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ extension LintPipeline {
2828
func visitIfEnabled<Rule: SyntaxLintRule, Node: SyntaxProtocol>(
2929
_ visitor: (Rule) -> (Node) -> SyntaxVisitorContinueKind, for node: Node
3030
) {
31-
guard context.isRuleEnabled(Rule.self, node: Syntax(node)) else { return }
31+
guard context.shouldFormat(Rule.self, node: Syntax(node)) else { return }
3232
let ruleId = ObjectIdentifier(Rule.self)
3333
guard self.shouldSkipChildren[ruleId] == nil else { return }
3434
let rule = self.rule(Rule.self)
@@ -54,7 +54,7 @@ extension LintPipeline {
5454
// more importantly because the `visit` methods return protocol refinements of `Syntax` that
5555
// cannot currently be expressed as constraints without duplicating this function for each of
5656
// them individually.
57-
guard context.isRuleEnabled(Rule.self, node: Syntax(node)) else { return }
57+
guard context.shouldFormat(Rule.self, node: Syntax(node)) else { return }
5858
guard self.shouldSkipChildren[ObjectIdentifier(Rule.self)] == nil else { return }
5959
let rule = self.rule(Rule.self)
6060
_ = visitor(rule)(node)

Sources/SwiftFormat/Core/SyntaxFormatRule.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class SyntaxFormatRule: SyntaxRewriter, Rule {
3232
public override func visitAny(_ node: Syntax) -> Syntax? {
3333
// If the rule is not enabled, then return the node unmodified; otherwise, returning nil tells
3434
// SwiftSyntax to continue with the standard dispatch.
35-
guard context.isRuleEnabled(type(of: self), node: node) else { return node }
35+
guard context.shouldFormat(type(of: self), node: node) else { return node }
3636
return nil
3737
}
3838
}

Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift

+66-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import SwiftSyntax
14+
import Foundation
1415

1516
/// PrettyPrinter takes a Syntax node and outputs a well-formatted, re-indented reproduction of the
1617
/// code as a String.
@@ -66,6 +67,17 @@ public class PrettyPrinter {
6667
private var configuration: Configuration { return context.configuration }
6768
private let maxLineLength: Int
6869
private var tokens: [Token]
70+
private var source: String
71+
72+
/// Keep track of where formatting was disabled in the original source
73+
///
74+
/// To format a selection, we insert `enableFormatting`/`disableFormatting` tokens into the
75+
/// stream when entering/exiting a selection range. Those tokens include utf8 offsets into the
76+
/// original source. When enabling formatting, we copy the text between `disabledPosition` and the
77+
/// current position to `outputBuffer`. From then on, we continue to format until the next
78+
/// `disableFormatting` token.
79+
private var disabledPosition: AbsolutePosition? = nil
80+
6981
private var outputBuffer: String = ""
7082

7183
/// The number of spaces remaining on the current line.
@@ -172,11 +184,14 @@ public class PrettyPrinter {
172184
/// - printTokenStream: Indicates whether debug information about the token stream should be
173185
/// printed to standard output.
174186
/// - whitespaceOnly: Whether only whitespace changes should be made.
175-
public init(context: Context, node: Syntax, printTokenStream: Bool, whitespaceOnly: Bool) {
187+
public init(context: Context, source: String, node: Syntax, printTokenStream: Bool, whitespaceOnly: Bool) {
176188
self.context = context
189+
self.source = source
177190
let configuration = context.configuration
178191
self.tokens = node.makeTokenStream(
179-
configuration: configuration, operatorTable: context.operatorTable)
192+
configuration: configuration,
193+
selection: context.selection,
194+
operatorTable: context.operatorTable)
180195
self.maxLineLength = configuration.lineLength
181196
self.spaceRemaining = self.maxLineLength
182197
self.printTokenStream = printTokenStream
@@ -187,7 +202,9 @@ public class PrettyPrinter {
187202
///
188203
/// No further processing is performed on the string.
189204
private func writeRaw<S: StringProtocol>(_ str: S) {
190-
outputBuffer.append(String(str))
205+
if disabledPosition == nil {
206+
outputBuffer.append(String(str))
207+
}
191208
}
192209

193210
/// Writes newlines into the output stream, taking into account any preexisting consecutive
@@ -241,7 +258,7 @@ public class PrettyPrinter {
241258
writeRaw(currentIndentation.indentation())
242259
spaceRemaining = maxLineLength - currentIndentation.length(in: configuration)
243260
isAtStartOfLine = false
244-
} else if pendingSpaces > 0 {
261+
} else if pendingSpaces > 0 {
245262
writeRaw(String(repeating: " ", count: pendingSpaces))
246263
}
247264
writeRaw(text)
@@ -569,6 +586,39 @@ public class PrettyPrinter {
569586
write(",")
570587
spaceRemaining -= 1
571588
}
589+
590+
case .enableFormatting(let enabledPosition):
591+
guard let disabledPosition else {
592+
// if we're not disabled, we ignore the token
593+
break
594+
}
595+
let start = source.utf8.index(source.utf8.startIndex, offsetBy: disabledPosition.utf8Offset)
596+
let end: String.Index
597+
if let enabledPosition {
598+
end = source.utf8.index(source.utf8.startIndex, offsetBy: enabledPosition.utf8Offset)
599+
} else {
600+
end = source.endIndex
601+
}
602+
var text = String(source[start..<end])
603+
// strip trailing whitespace so that the next formatting can add the right amount
604+
if let nonWhitespace = text.rangeOfCharacter(
605+
from: CharacterSet.whitespaces.inverted, options: .backwards) {
606+
text = String(text[..<nonWhitespace.upperBound])
607+
}
608+
609+
self.disabledPosition = nil
610+
writeRaw(text)
611+
if text.hasSuffix("\n") {
612+
isAtStartOfLine = true
613+
consecutiveNewlineCount = 1
614+
} else {
615+
isAtStartOfLine = false
616+
consecutiveNewlineCount = 0
617+
}
618+
619+
case .disableFormatting(let newPosition):
620+
assert(disabledPosition == nil)
621+
disabledPosition = newPosition
572622
}
573623
}
574624

@@ -673,6 +723,10 @@ public class PrettyPrinter {
673723
let length = isSingleElement ? 0 : 1
674724
total += length
675725
lengths.append(length)
726+
727+
case .enableFormatting, .disableFormatting:
728+
// no effect on length calculations
729+
lengths.append(0)
676730
}
677731
}
678732

@@ -775,6 +829,14 @@ public class PrettyPrinter {
775829
case .contextualBreakingEnd:
776830
printDebugIndent()
777831
print("[END BREAKING CONTEXT Idx: \(idx)]")
832+
833+
case .enableFormatting(let pos):
834+
printDebugIndent()
835+
print("[ENABLE FORMATTING utf8 offset: \(String(describing: pos))]")
836+
837+
case .disableFormatting(let pos):
838+
printDebugIndent()
839+
print("[DISABLE FORMATTING utf8 offset: \(pos)]")
778840
}
779841
}
780842

0 commit comments

Comments
 (0)