Skip to content

Commit 96c7ddc

Browse files
committed
Add edits to FixIt for sourcekit-lsp to access
Finetue the code Refine the code Finetune the code Add unit tests Finetune the code
1 parent e5962c4 commit 96c7ddc

File tree

5 files changed

+167
-105
lines changed

5 files changed

+167
-105
lines changed

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,48 @@ public struct FixIt {
4646
self.changes = changes
4747
}
4848
}
49+
50+
public extension FixIt {
51+
var edits: [SourceEdit] {
52+
var existingEdits = [SourceEdit]()
53+
for change in changes {
54+
let edit = edit(from: change)
55+
var isOverlapping = false
56+
for existingEdit in existingEdits {
57+
// The edit overlaps with the previous edit. We can't apply both
58+
// without conflicts. Apply the one that's listed first and drop the
59+
// later edit.
60+
if edit.range.overlaps(existingEdit.range) {
61+
isOverlapping = true
62+
break
63+
}
64+
}
65+
if !isOverlapping {
66+
existingEdits.append(edit)
67+
}
68+
}
69+
return existingEdits
70+
}
71+
72+
private func edit(from change: Change) -> SourceEdit {
73+
switch change {
74+
case .replace(let oldNode, let newNode):
75+
return SourceEdit(
76+
range: oldNode.position..<oldNode.endPosition,
77+
replacement: newNode.description
78+
)
79+
80+
case .replaceLeadingTrivia(let token, let newTrivia):
81+
return SourceEdit(
82+
range: token.position..<token.positionAfterSkippingLeadingTrivia,
83+
replacement: newTrivia.description
84+
)
85+
86+
case .replaceTrailingTrivia(let token, let newTrivia):
87+
return SourceEdit(
88+
range: token.endPositionBeforeTrailingTrivia..<token.endPosition,
89+
replacement: newTrivia.description
90+
)
91+
}
92+
}
93+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import SwiftSyntax
2+
3+
/// A textual edit to the original source represented by a range and a
4+
/// replacement.
5+
public struct SourceEdit: Equatable {
6+
/// The half-open range that this edit applies to.
7+
public let range: Range<AbsolutePosition>
8+
/// The text to replace the original range with. Empty for a deletion.
9+
public let replacement: String
10+
11+
/// Length of the original source range that this edit applies to. Zero if
12+
/// this is an addition.
13+
public var length: SourceLength {
14+
return SourceLength(utf8Length: range.lowerBound.utf8Offset - range.upperBound.utf8Offset)
15+
}
16+
17+
/// Create an edit to replace `range` in the original source with
18+
/// `replacement`.
19+
public init(range: Range<AbsolutePosition>, replacement: String) {
20+
self.range = range
21+
self.replacement = replacement
22+
}
23+
24+
/// Convenience function to create a textual addition after the given node
25+
/// and its trivia.
26+
public static func insert(_ newText: String, after node: some SyntaxProtocol) -> SourceEdit {
27+
return SourceEdit(range: node.endPosition..<node.endPosition, replacement: newText)
28+
}
29+
30+
/// Convenience function to create a textual addition before the given node
31+
/// and its trivia.
32+
public static func insert(_ newText: String, before node: some SyntaxProtocol) -> SourceEdit {
33+
return SourceEdit(range: node.position..<node.position, replacement: newText)
34+
}
35+
36+
/// Convenience function to create a textual replacement of the given node,
37+
/// including its trivia.
38+
public static func replace(_ node: some SyntaxProtocol, with replacement: String) -> SourceEdit {
39+
return SourceEdit(range: node.position..<node.endPosition, replacement: replacement)
40+
}
41+
42+
/// Convenience function to create a textual deletion the given node and its
43+
/// trivia.
44+
public static func remove(_ node: some SyntaxProtocol) -> SourceEdit {
45+
return SourceEdit(range: node.position..<node.endPosition, replacement: "")
46+
}
47+
}
48+
49+
extension SourceEdit: CustomDebugStringConvertible {
50+
public var debugDescription: String {
51+
let hasNewline = replacement.contains { $0.isNewline }
52+
if hasNewline {
53+
return #"""
54+
\#(range.lowerBound.utf8Offset)-\#(range.upperBound.utf8Offset)
55+
"""
56+
\#(replacement)
57+
"""
58+
"""#
59+
}
60+
return "\(range.lowerBound.utf8Offset)-\(range.upperBound.utf8Offset) \"\(replacement)\""
61+
}
62+
}

Sources/SwiftRefactor/RefactoringProvider.swift

Lines changed: 4 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import SwiftSyntax
14+
import SwiftDiagnostics
15+
16+
@available(*, deprecated, message: "SourceEdit has been moved to the SwiftDiagnostics module")
17+
public typealias SourceEdit = SwiftDiagnostics.SourceEdit
1418

1519
/// A refactoring expressed as textual edits on the original syntax tree. In
1620
/// general clients should prefer `SyntaxRefactoringProvider` where possible.
@@ -109,64 +113,3 @@ extension SyntaxRefactoringProvider {
109113
return [SourceEdit.replace(syntax, with: output.description)]
110114
}
111115
}
112-
113-
/// A textual edit to the original source represented by a range and a
114-
/// replacement.
115-
public struct SourceEdit: Equatable {
116-
/// The half-open range that this edit applies to.
117-
public let range: Range<AbsolutePosition>
118-
/// The text to replace the original range with. Empty for a deletion.
119-
public let replacement: String
120-
121-
/// Length of the original source range that this edit applies to. Zero if
122-
/// this is an addition.
123-
public var length: SourceLength {
124-
return SourceLength(utf8Length: range.lowerBound.utf8Offset - range.upperBound.utf8Offset)
125-
}
126-
127-
/// Create an edit to replace `range` in the original source with
128-
/// `replacement`.
129-
public init(range: Range<AbsolutePosition>, replacement: String) {
130-
self.range = range
131-
self.replacement = replacement
132-
}
133-
134-
/// Convenience function to create a textual addition after the given node
135-
/// and its trivia.
136-
public static func insert(_ newText: String, after node: some SyntaxProtocol) -> SourceEdit {
137-
return SourceEdit(range: node.endPosition..<node.endPosition, replacement: newText)
138-
}
139-
140-
/// Convenience function to create a textual addition before the given node
141-
/// and its trivia.
142-
public static func insert(_ newText: String, before node: some SyntaxProtocol) -> SourceEdit {
143-
return SourceEdit(range: node.position..<node.position, replacement: newText)
144-
}
145-
146-
/// Convenience function to create a textual replacement of the given node,
147-
/// including its trivia.
148-
public static func replace(_ node: some SyntaxProtocol, with replacement: String) -> SourceEdit {
149-
return SourceEdit(range: node.position..<node.endPosition, replacement: replacement)
150-
}
151-
152-
/// Convenience function to create a textual deletion the given node and its
153-
/// trivia.
154-
public static func remove(_ node: some SyntaxProtocol) -> SourceEdit {
155-
return SourceEdit(range: node.position..<node.endPosition, replacement: "")
156-
}
157-
}
158-
159-
extension SourceEdit: CustomDebugStringConvertible {
160-
public var debugDescription: String {
161-
let hasNewline = replacement.contains { $0.isNewline }
162-
if hasNewline {
163-
return #"""
164-
\#(range.lowerBound.utf8Offset)-\#(range.upperBound.utf8Offset)
165-
"""
166-
\#(replacement)
167-
"""
168-
"""#
169-
}
170-
return "\(range.lowerBound.utf8Offset)-\(range.upperBound.utf8Offset) \"\(replacement)\""
171-
}
172-
}

Sources/_SwiftSyntaxTestSupport/FixItApplier.swift

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,6 @@ import SwiftDiagnostics
1414
import SwiftSyntax
1515

1616
public enum FixItApplier {
17-
struct Edit: Equatable {
18-
var startUtf8Offset: Int
19-
var endUtf8Offset: Int
20-
let replacement: String
21-
22-
var replacementLength: Int {
23-
return replacement.utf8.count
24-
}
25-
26-
var replacementRange: Range<Int> {
27-
return startUtf8Offset..<endUtf8Offset
28-
}
29-
}
30-
3117
/// Applies selected or all Fix-Its from the provided diagnostics to a given syntax tree.
3218
///
3319
/// - Parameters:
@@ -44,13 +30,12 @@ public enum FixItApplier {
4430
) -> String {
4531
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
4632

47-
let changes =
33+
var edits =
4834
diagnostics
4935
.flatMap(\.fixIts)
5036
.filter { messages.contains($0.message.message) }
51-
.flatMap(\.changes)
37+
.flatMap(\.edits)
5238

53-
var edits: [Edit] = changes.map(\.edit)
5439
var source = tree.description
5540

5641
while let edit = edits.first {
@@ -61,9 +46,7 @@ public enum FixItApplier {
6146

6247
source.replaceSubrange(startIndex..<endIndex, with: edit.replacement)
6348

64-
edits = edits.compactMap { remainingEdit -> FixItApplier.Edit? in
65-
var remainingEdit = remainingEdit
66-
49+
edits = edits.compactMap { remainingEdit -> SourceEdit? in
6750
if remainingEdit.replacementRange.overlaps(edit.replacementRange) {
6851
// The edit overlaps with the previous edit. We can't apply both
6952
// without conflicts. Apply the one that's listed first and drop the
@@ -74,8 +57,9 @@ public enum FixItApplier {
7457
// If the remaining edit starts after or at the end of the edit that we just applied,
7558
// shift it by the current edit's difference in length.
7659
if edit.endUtf8Offset <= remainingEdit.startUtf8Offset {
77-
remainingEdit.startUtf8Offset = remainingEdit.startUtf8Offset - edit.replacementRange.count + edit.replacementLength
78-
remainingEdit.endUtf8Offset = remainingEdit.endUtf8Offset - edit.replacementRange.count + edit.replacementLength
60+
let startPosition = AbsolutePosition(utf8Offset: remainingEdit.startUtf8Offset - edit.replacementRange.count + edit.replacementLength)
61+
let endPosition = AbsolutePosition(utf8Offset: remainingEdit.endUtf8Offset - edit.replacementRange.count + edit.replacementLength)
62+
return SourceEdit(range: startPosition..<endPosition, replacement: remainingEdit.replacement)
7963
}
8064

8165
return remainingEdit
@@ -86,29 +70,20 @@ public enum FixItApplier {
8670
}
8771
}
8872

89-
fileprivate extension FixIt.Change {
90-
var edit: FixItApplier.Edit {
91-
switch self {
92-
case .replace(let oldNode, let newNode):
93-
return FixItApplier.Edit(
94-
startUtf8Offset: oldNode.position.utf8Offset,
95-
endUtf8Offset: oldNode.endPosition.utf8Offset,
96-
replacement: newNode.description
97-
)
73+
private extension SourceEdit {
74+
var startUtf8Offset: Int {
75+
return range.lowerBound.utf8Offset
76+
}
77+
78+
var endUtf8Offset: Int {
79+
return range.upperBound.utf8Offset
80+
}
9881

99-
case .replaceLeadingTrivia(let token, let newTrivia):
100-
return FixItApplier.Edit(
101-
startUtf8Offset: token.position.utf8Offset,
102-
endUtf8Offset: token.positionAfterSkippingLeadingTrivia.utf8Offset,
103-
replacement: newTrivia.description
104-
)
82+
var replacementLength: Int {
83+
return replacement.utf8.count
84+
}
10585

106-
case .replaceTrailingTrivia(let token, let newTrivia):
107-
return FixItApplier.Edit(
108-
startUtf8Offset: token.endPositionBeforeTrailingTrivia.utf8Offset,
109-
endUtf8Offset: token.endPosition.utf8Offset,
110-
replacement: newTrivia.description
111-
)
112-
}
86+
var replacementRange: Range<Int> {
87+
return startUtf8Offset..<endUtf8Offset
11388
}
11489
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import SwiftParser
2+
import SwiftParserDiagnostics
3+
import SwiftSyntax
4+
import XCTest
5+
import _SwiftSyntaxTestSupport
6+
7+
final class FixItTests: XCTestCase {
8+
func testEditsForFixIt() {
9+
let testCases: [[(range: Range<Int>, replacement: String)]] = [
10+
[(9..<15, "Multiident "), (15..<21, "")],
11+
[(9..<15, "MultiIdent "), (15..<21, "")]
12+
]
13+
14+
let markedSource = "protocol Multi 1️⃣ident {}"
15+
let (_, source) = extractMarkers(markedSource)
16+
let tree = Parser.parse(source: source)
17+
let diags = ParseDiagnosticsGenerator.diagnostics(for: tree)
18+
XCTAssertEqual(diags.count, 1)
19+
let diag = diags.first!
20+
XCTAssertEqual(diag.fixIts.count, 2)
21+
22+
for i in 0..<diag.fixIts.count {
23+
let fixIt = diag.fixIts[i]
24+
let changes = fixIt.changes
25+
let edits = fixIt.edits
26+
XCTAssertNotEqual(changes.count, edits.count)
27+
let expctedEdits = testCases[i]
28+
XCTAssertEqual(expctedEdits.count, edits.count)
29+
for j in 0..<edits.count {
30+
let edit = edits[j]
31+
let expectedEdit = expctedEdits[j]
32+
XCTAssertEqual(edit.range, AbsolutePosition(utf8Offset: expectedEdit.range.lowerBound)..<AbsolutePosition(utf8Offset: expectedEdit.range.upperBound))
33+
XCTAssertEqual(edit.replacement, expectedEdit.replacement)
34+
}
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)