Skip to content

Commit 5aa23a6

Browse files
authored
Merge pull request swiftlang#238 from benlangmuir/notes-and-multiples
[fixit] Add support for fixits that come from notes, and multi-edit fixes
2 parents 33dd191 + fa8acc1 commit 5aa23a6

File tree

6 files changed

+333
-34
lines changed

6 files changed

+333
-34
lines changed

Sources/LSPLogging/Logging.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ public func logAsync(level: LogLevel = .default, messageProducer: @escaping (_ c
3737
Logger.shared.logAsync(level: level, messageProducer: messageProducer)
3838
}
3939

40+
/// Log an error and trigger an assertion failure (if compiled with assertions).
41+
///
42+
/// If `level >= Logger.shared.currentLevel`, it will be emitted. However, the converse is not necessarily true: on platforms that provide `os_log`, the message may be emitted by `os_log` according to its own rules about log level.
43+
///
44+
/// - parameter message: The message to print.
45+
public func logAssertionFailure(_ message: String, file: StaticString = #file, line: UInt = #line) {
46+
Logger.shared.log(message, level: .error)
47+
assertionFailure(message, file: file, line: line)
48+
}
49+
4050
/// Like `try?`, but logs the error on failure.
4151
public func orLog<R>(
4252
_ prefix: String = "",

Sources/LanguageServerProtocol/SupportTypes/Diagnostic.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,14 @@ public struct DiagnosticRelatedInformation: Codable, Hashable {
7777

7878
public var message: String
7979

80-
public init(location: Location, message: String) {
80+
/// All the code actions that address the parent diagnostic via this note.
81+
/// **LSP Extension from clangd**.
82+
public var codeActions: [CodeAction]?
83+
84+
public init(location: Location, message: String, codeActions: [CodeAction]?) {
8185
self.location = location
8286
self.message = message
87+
self.codeActions = codeActions
8388
}
8489
}
8590

Sources/SourceKit/sourcekitd/Diagnostic.swift

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,56 @@ import SKSupport
1616
import sourcekitd
1717

1818
extension CodeAction {
19-
init?(fixit: SKResponseDictionary, in snapshot: DocumentSnapshot) {
20-
let keys = fixit.sourcekitd.keys
2119

22-
guard let utf8Offset: Int = fixit[keys.offset],
23-
let length: Int = fixit[keys.length],
24-
let replacement: String = fixit[keys.sourcetext],
25-
let position = snapshot.positionOf(utf8Offset: utf8Offset),
26-
let endPosition = snapshot.positionOf(utf8Offset: utf8Offset + length),
27-
let startIndex = snapshot.indexOf(utf8Offset: utf8Offset),
28-
let endIndex = snapshot.indexOf(utf8Offset: utf8Offset + length),
29-
length > 0 || !replacement.isEmpty
30-
else {
20+
/// Creates a CodeAction from a list for sourcekit fixits.
21+
///
22+
/// If this is from a note, the note's description should be passed as `fromNote`.
23+
init?(fixits: SKResponseArray, in snapshot: DocumentSnapshot, fromNote: String?) {
24+
var edits: [TextEdit] = []
25+
let editsMapped = fixits.forEach { (_, skfixit) -> Bool in
26+
if let edit = TextEdit(fixit: skfixit, in: snapshot) {
27+
edits.append(edit)
28+
return true
29+
}
30+
return false
31+
}
32+
33+
if !editsMapped {
34+
log("failed to construct TextEdits from response \(fixits)", level: .warning)
3135
return nil
3236
}
3337

34-
let range = position..<endPosition
35-
let original = String(snapshot.text[startIndex..<endIndex])
36-
let title = Self.fixitTitle(replace: original, with: replacement)
37-
let workspaceEdit = WorkspaceEdit(
38-
changes: [snapshot.document.uri:[TextEdit(range: range, newText: replacement)]])
38+
if edits.isEmpty {
39+
return nil
40+
}
41+
42+
let title: String
43+
if let fromNote = fromNote {
44+
title = fromNote
45+
} else {
46+
guard let startIndex = snapshot.index(of: edits[0].range.lowerBound),
47+
let endIndex = snapshot.index(of: edits[0].range.upperBound),
48+
startIndex <= endIndex,
49+
snapshot.text.indices.contains(startIndex),
50+
endIndex <= snapshot.text.endIndex
51+
else {
52+
logAssertionFailure("position mapped, but indices failed for edit range \(edits[0])")
53+
return nil
54+
}
55+
let oldText = String(snapshot.text[startIndex..<endIndex])
56+
let description = Self.fixitTitle(replace: oldText, with: edits[0].newText)
57+
if edits.count == 1 {
58+
title = description
59+
} else {
60+
title = description + "..."
61+
}
62+
}
3963

4064
self.init(
4165
title: title,
4266
kind: .quickFix,
4367
diagnostics: nil,
44-
edit: workspaceEdit)
68+
edit: WorkspaceEdit(changes: [snapshot.document.uri:edits]))
4569
}
4670

4771
/// Describe a fixit's edit briefly.
@@ -61,6 +85,25 @@ extension CodeAction {
6185
}
6286
}
6387

88+
extension TextEdit {
89+
90+
/// Creates a TextEdit from a sourcekitd fixit response dictionary.
91+
init?(fixit: SKResponseDictionary, in snapshot: DocumentSnapshot) {
92+
let keys = fixit.sourcekitd.keys
93+
if let utf8Offset: Int = fixit[keys.offset],
94+
let length: Int = fixit[keys.length],
95+
let replacement: String = fixit[keys.sourcetext],
96+
let position = snapshot.positionOf(utf8Offset: utf8Offset),
97+
let endPosition = snapshot.positionOf(utf8Offset: utf8Offset + length),
98+
length > 0 || !replacement.isEmpty
99+
{
100+
self.init(range: position..<endPosition, newText: replacement)
101+
} else {
102+
return nil
103+
}
104+
}
105+
}
106+
64107
extension Diagnostic {
65108

66109
/// Creates a diagnostic from a sourcekitd response dictionary.
@@ -98,26 +141,18 @@ extension Diagnostic {
98141
}
99142
}
100143

101-
var fixits: [CodeAction]? = nil
102-
if let skfixits: SKResponseArray = diag[keys.fixits] {
103-
fixits = []
104-
skfixits.forEach { (_, skfixit) -> Bool in
105-
if let codeAction = CodeAction(fixit: skfixit, in: snapshot) {
106-
fixits?.append(codeAction)
107-
}
108-
return true
109-
}
144+
var actions: [CodeAction]? = nil
145+
if let skfixits: SKResponseArray = diag[keys.fixits],
146+
let action = CodeAction(fixits: skfixits, in: snapshot, fromNote: nil) {
147+
actions = [action]
110148
}
111149

112150
var notes: [DiagnosticRelatedInformation]? = nil
113151
if let sknotes: SKResponseArray = diag[keys.diagnostics] {
114152
notes = []
115153
sknotes.forEach { (_, sknote) -> Bool in
116-
guard let note = Diagnostic(sknote, in: snapshot) else { return true }
117-
notes?.append(DiagnosticRelatedInformation(
118-
location: Location(uri: snapshot.document.uri, range: note.range),
119-
message: note.message
120-
))
154+
guard let note = DiagnosticRelatedInformation(sknote, in: snapshot) else { return true }
155+
notes?.append(note)
121156
return true
122157
}
123158
}
@@ -129,7 +164,42 @@ extension Diagnostic {
129164
source: "sourcekitd",
130165
message: message,
131166
relatedInformation: notes,
132-
codeActions: fixits)
167+
codeActions: actions)
168+
}
169+
}
170+
171+
extension DiagnosticRelatedInformation {
172+
173+
/// Creates related information from a sourcekitd note response dictionary.
174+
init?(_ diag: SKResponseDictionary, in snapshot: DocumentSnapshot) {
175+
let keys = diag.sourcekitd.keys
176+
177+
var position: Position? = nil
178+
if let line: Int = diag[keys.line],
179+
let utf8Column: Int = diag[keys.column],
180+
line > 0, utf8Column > 0
181+
{
182+
position = snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: utf8Column - 1)
183+
} else if let utf8Offset: Int = diag[keys.offset] {
184+
position = snapshot.positionOf(utf8Offset: utf8Offset)
185+
}
186+
187+
if position == nil {
188+
return nil
189+
}
190+
191+
guard let message: String = diag[keys.description] else { return nil }
192+
193+
var actions: [CodeAction]? = nil
194+
if let skfixits: SKResponseArray = diag[keys.fixits],
195+
let action = CodeAction(fixits: skfixits, in: snapshot, fromNote: message) {
196+
actions = [action]
197+
}
198+
199+
self.init(
200+
location: Location(uri: snapshot.document.uri, range: Range(position!)),
201+
message: message,
202+
codeActions: actions)
133203
}
134204
}
135205

Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,11 @@ extension SwiftLanguageServer {
10871087
let codeActions = cachedDiags.flatMap { (cachedDiag) -> [CodeAction] in
10881088
let diag = cachedDiag.diagnostic
10891089

1090-
guard let codeActions = diag.codeActions else {
1090+
let codeActions: [CodeAction] =
1091+
(diag.codeActions ?? []) +
1092+
(diag.relatedInformation?.flatMap{ $0.codeActions ?? [] } ?? [])
1093+
1094+
if codeActions.isEmpty {
10911095
// The diagnostic doesn't have fix-its. Don't return anything.
10921096
return []
10931097
}
@@ -1115,6 +1119,13 @@ extension SwiftLanguageServer {
11151119
var codeAction = $0
11161120
var diagnosticWithoutCodeActions = diag
11171121
diagnosticWithoutCodeActions.codeActions = nil
1122+
if let related = diagnosticWithoutCodeActions.relatedInformation {
1123+
diagnosticWithoutCodeActions.relatedInformation = related.map {
1124+
var withoutCodeActions = $0
1125+
withoutCodeActions.codeActions = nil
1126+
return withoutCodeActions
1127+
}
1128+
}
11181129
codeAction.diagnostics = [diagnosticWithoutCodeActions]
11191130
return codeAction
11201131
})

0 commit comments

Comments
 (0)