8000 Add edits to FixIt for sourcekit-lsp to access · swiftlang/swift-syntax@1d84c20 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1d84c20

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 Update the code per comments Simplify the test Move the SourceEdit.swift from module SwiftDiagnostics to module SwiftSyntax Add copyright to new files Update unit tests to align the convention and update release noten Finetune the code Update release note Add missing SourceEdit.swift to CMakelists.txt Format the code
1 parent 7cff260 commit 1d84c20

File tree

7 files changed

+185
-105
lines changed

7 files changed

+185
-105
lines changed

Release Notes/511.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Swift Syntax 511 Release Notes
22

33
## New APIs
4+
- FixIt now has a new computed propery named edits
5+
- Description: the edits represent the non-overlapping textual edits that need to be performed when the Fix-It is applied.
6+
- Issue: https://github.com/apple/sourcekit-lsp/issues/909
7+
- Pull Request: https://github.com/apple/swift-syntax/pull/2314
8+
9+
- SourceEdit
10+
- Description: SourceEdit has been moved from SwiftRefactor to SwiftSyntax
11+
- Issue: https://github.com/apple/sourcekit-lsp/issues/909
12+
- Pull Request: https://github.com/apple/swift-syntax/pull/2314
413

514
## API Behavior Changes
615

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,45 @@ public struct FixIt {
4646
self.changes = changes
4747
}
4848
}
49+
50+
extension FixIt {
51+
/// The edits represent the non-overlapping textual edits that need to be performed when the Fix-It is applied.
52+
public var edits: [SourceEdit] {
53+
var existingEdits = [SourceEdit]()
54+
for change in changes {
55+
let edit = change.edit
56+
let isOverlapping = existingEdits.contains { edit.range.overlaps($0.range) }
57+
if !isOverlapping {
58+
// The edit overlaps with the previous edit. We can't apply both
59+
// without conflicts. Apply the one that's listed first and drop the
60+
// later edit.
61+
existingEdits.append(edit)
62+
}
63+
}
64+
return existingEdits
65+
}
66+
}
67+
68+
private extension FixIt.Change {
69+
var edit: SourceEdit {
70+
switch self {
71+
case .replace(let oldNode, let newNode):
72+
return SourceEdit(
73+
range: oldNode.position..<oldNode.endPosition,
74+
replacement: newNode.description
75+
)
76+
77+
case .replaceLeadingTrivia(let token, let newTrivia):
78+
return SourceEdit(
79+
range: token.position..<token.positionAfterSkippingLeadingTrivia,
80+
replacement: newTrivia.description
81+
)
82+
83+
case .replaceTrailingTrivia(let token, let newTrivia):
84+
return SourceEdit(
85+
range: token.endPositionBeforeTrailingTrivia..<token.endPosition,
86+
replacement: newTrivia.description
87+
)
88+
}
89+
}
90+
}

Sources/SwiftRefactor/RefactoringProvider.swift

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -109,64 +109,3 @@ extension SyntaxRefactoringProvider {
109109
return [SourceEdit.replace(syntax, with: output.description)]
110110
}
111111
}
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< 10000 /span> 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/SwiftSyntax/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ add_swift_syntax_library(SwiftSyntax
1616
Convenience.swift
1717
MemoryLayout.swift
1818
MissingNodeInitializers.swift
19+
SourceEdit.swift
1920
SourceLength.swift
2021
SourceLocation.swift
2122
SourcePresence.swift

Sources/SwiftSyntax/SourceEdit.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
/// A textual edit to the original source represented by a range and a
14+
/// replacement.
15+
public struct SourceEdit: Equatable {
16+
/// The half-open range that this edit applies to.
17+
public let range: Range<AbsolutePosition>
18+
/// The text to replace the original range with. Empty for a deletion.
19+
public let replacement: String
20+
21+
/// Length of the original source range that this edit applies to. Zero if
22+
/// this is an addition.
23+
public var length: SourceLength {
24+
return SourceLength(utf8Length: range.lowerBound.utf8Offset - range.upperBound.utf8Offset)
25+
}
26+
27+
/// Create an edit to replace `range` in the original source with
28+
/// `replacement`.
29+
public init(range: Range<AbsolutePosition>, replacement: String) {
30+
self.range = range
31+
self.replacement = replacement
32+
}
33+
34+
/// Convenience function to create a textual addition after the given node
35+
/// and its trivia.
36+
public static func insert(_ newText: String, after node: some SyntaxProtocol) -> SourceEdit {
37+
return SourceEdit(range: node.endPosition..<node.endPosition, replacement: newText)
38+
}
39+
40+
/// Convenience function to create a textual addition before the given node
41+
/// and its trivia.
42+
public static func insert(_ newText: String, before node: some SyntaxProtocol) -> SourceEdit {
43+
return SourceEdit(range: node.position..<node.position, replacement: newText)
44+
}
45+
46+
/// Convenience function to create a textual replacement of the given node,
47+
/// including its trivia.
48+
public static func replace(_ node: some SyntaxProtocol, with replacement: String) -> SourceEdit {
49+
return SourceEdit(range: node.position..<node.endPosition, replacement: replacement)
50+
}
51+
52+
/// Convenience function to create a textual deletion the given node and its
53+
/// trivia.
54+
public static func remove(_ node: some SyntaxProtocol) -> SourceEdit {
55+
return SourceEdit(range: node.position..<node.endPosition, replacement: "")
56+
}
57+
}
58+
59+
extension SourceEdit: CustomDebugStringConvertible {
60+
public var debugDescription: String {
61+
let hasNewline = replacement.contains { $0.isNewline }
62+
if hasNewline {
63+
return #"""
64+
\#(range.lowerBound.utf8Offset)-\#(range.upperBound.utf8Offset)
65+
"""
66+
\#(replacement)
67+
"""
68+
"""#
69+
}
70+
return "\(range.lowerBound.utf8Offset)-\(range.upperBound.utf8Offset) \"\(replacement)\""
71+
}
72+
}

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< 10000 span class=pl-kos>: 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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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 SwiftParser
14+
import SwiftParserDiagnostics
15+
import SwiftSyntax
16+
import XCTest
17+
import _SwiftSyntaxTestSupport
18+
19+
final class FixItTests: XCTestCase {
20+
func testEditsForFixIt() throws {
21+
let markedSource = "protocol 1️⃣Multi 2️⃣ident 3️⃣{}"
22+
let (markers, source) = extractMarkers(markedSource)
23+
let positions = markers.mapValues { AbsolutePosition(utf8Offset: $0) }
24+
XCTAssertEqual(positions.count, 3)
25+
26+
let expectedEdits = [
27+
SourceEdit(range: positions["1️⃣"]!..<positions["2️⃣"]!, replacement: "Multiident "),
28+
SourceEdit(range: positions["2️⃣"]!..<positions["3️⃣"]!, replacement: ""),
29+
]
30+
let tree = Parser.parse(source: source)
31+
let diags = ParseDiagnosticsGenerator.diagnostics(for: tree)
32+
XCTAssertEqual(diags.count, 1)
33+
let diag = try XCTUnwrap(diags.first)
34+
XCTAssertEqual(diag.fixIts.count, 2)
35+
36+
let fixIt = try XCTUnwrap(diag.fixIts.first)
37+
let changes = fixIt.changes
38+
let edits = fixIt.edits
39+
XCTAssertNotEqual(changes.count, edits.count)
40+
XCTAssertEqual(expectedEdits, edits)
41+
}
42+
}

0 commit comments

Comments
 (0)
0