Skip to content

Commit 5833322

Browse files
authored
Merge pull request #1631 from ahoppen/nested-macro-expansions
Support expansion of nested macros
2 parents 900db4c + 90e0f3f commit 5833322

14 files changed

+924
-492
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ target_sources(SourceKitLSP PRIVATE
5151
Swift/OpenInterface.swift
5252
Swift/RefactoringResponse.swift
5353
Swift/RefactoringEdit.swift
54-
Swift/RefactorCommand.swift
5554
Swift/ReferenceDocumentURL.swift
5655
Swift/RelatedIdentifiers.swift
5756
Swift/RewriteSourceKitPlaceholders.swift

Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ enum MessageHandlingDependencyTracker: DependencyTracker {
179179
} else {
180180
self = .freestanding
181181
}
182+
case let request as GetReferenceDocumentRequest:
183+
self = .documentRequest(request.uri)
182184
case is InitializeRequest:
183185
self = .globalConfigurationChange
184186
case is InlayHintRefreshRequest:

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ package actor SourceKitLSPServer {
301301
}
302302

303303
package func workspaceForDocument(uri: DocumentURI) async -> Workspace? {
304+
let uri = uri.primaryFile ?? uri
304305
if let cachedWorkspace = self.uriToWorkspaceCache[uri]?.value {
305306
return cachedWorkspace
306307
}
@@ -1744,8 +1745,7 @@ extension SourceKitLSPServer {
17441745
}
17451746

17461747
func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse {
1747-
let referenceDocumentURL = try ReferenceDocumentURL(from: req.uri)
1748-
let primaryFileURI = referenceDocumentURL.primaryFile
1748+
let primaryFileURI = try ReferenceDocumentURL(from: req.uri).primaryFile
17491749

17501750
guard let workspace = await workspaceForDocument(uri: primaryFileURI) else {
17511751
throw ResponseError.workspaceNotOpen(primaryFileURI)

Sources/SourceKitLSP/Swift/ExpandMacroCommand.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
import LanguageServerProtocol
1414
import SourceKitD
1515

16-
package struct ExpandMacroCommand: RefactorCommand {
17-
typealias Response = MacroExpansion
18-
16+
package struct ExpandMacroCommand: SwiftCommand {
1917
package static let identifier: String = "expand.macro.command"
2018

2119
/// The name of this refactoring action.

Sources/SourceKitLSP/Swift/MacroExpansion.swift

Lines changed: 152 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import Crypto
1414
import Foundation
1515
import LanguageServerProtocol
1616
import SKLogging
17+
import SKOptions
18+
import SKSupport
1719
import SourceKitD
1820

1921
/// Detailed information about the result of a macro expansion operation.
@@ -44,6 +46,141 @@ struct MacroExpansion: RefactoringResponse {
4446
}
4547
}
4648

49+
/// Caches the contents of macro expansions that were recently requested by the user.
50+
actor MacroExpansionManager {
51+
private struct CacheEntry {
52+
// Key
53+
let snapshotID: DocumentSnapshot.ID
54+
let range: Range<Position>
55+
let buildSettings: SwiftCompileCommand?
56+
57+
// Value
58+
let value: [RefactoringEdit]
59+
60+
fileprivate init(
61+
snapshot: DocumentSnapshot,
62+
range: Range<Position>,
63+
buildSettings: SwiftCompileCommand?,
64+
value: [RefactoringEdit]
65+
) {
66+
self.snapshotID = snapshot.id
67+
self.range = range
68+
self.buildSettings = buildSettings
69+
self.value = value
70+
}
71+
}
72+
73+
init(swiftLanguageService: SwiftLanguageService?) {
74+
self.swiftLanguageService = swiftLanguageService
75+
}
76+
77+
private weak var swiftLanguageService: SwiftLanguageService?
78+
79+
/// The number of macro expansions to cache.
80+
///
81+
/// - Note: This should be bigger than the maximum expansion depth of macros a user might do to avoid re-generating
82+
/// all parent macros to a nested macro expansion's buffer. 10 seems to be big enough for that because it's
83+
/// unlikely that a macro will expand to more than 10 levels.
84+
private let cacheSize = 10
85+
86+
/// The cache that stores reportTasks for a combination of uri, range and build settings.
87+
///
88+
/// Conceptually, this is a dictionary. To prevent excessive memory usage we
89+
/// only keep `cacheSize` entries within the array. Older entries are at the
90+
/// end of the list, newer entries at the front.
91+
private var cache: [CacheEntry] = []
92+
93+
/// Return the text of the macro expansion referenced by `macroExpansionURLData`.
94+
func macroExpansion(
95+
for macroExpansionURLData: MacroExpansionReferenceDocumentURLData
96+
) async throws -> String {
97+
let expansions = try await macroExpansions(
98+
in: macroExpansionURLData.parent,
99+
at: macroExpansionURLData.selectionRange
100+
)
101+
guard let expansion = expansions.filter({ $0.bufferName == macroExpansionURLData.bufferName }).only else {
102+
throw ResponseError.unknown("Failed to find macro expansion for \(macroExpansionURLData.bufferName).")
103+
}
104+
return expansion.newText
105+
}
106+
107+
func macroExpansions(
108+
in uri: DocumentURI,
109+
at range: Range<Position>
110+
) async throws -> [RefactoringEdit] {
111+
guard let swiftLanguageService else {
112+
// `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do.
113+
throw ResponseError.unknown("Connection to the editor closed")
114+
}
115+
116+
let snapshot = try await swiftLanguageService.latestSnapshot(for: uri)
117+
let buildSettings = await swiftLanguageService.buildSettings(for: uri)
118+
119+
if let cacheEntry = cache.first(where: {
120+
$0.snapshotID == snapshot.id && $0.range == range && $0.buildSettings == buildSettings
121+
}) {
122+
return cacheEntry.value
123+
}
124+
let macroExpansions = try await macroExpansionsImpl(in: snapshot, at: range, buildSettings: buildSettings)
125+
cache.insert(
126+
CacheEntry(snapshot: snapshot, range: range, buildSettings: buildSettings, value: macroExpansions),
127+
at: 0
128+
)
129+
130+
while cache.count > cacheSize {
131+
cache.removeLast()
132+
}
133+
134+
return macroExpansions
135+
}
136+
137+
private func macroExpansionsImpl(
138+
in snapshot: DocumentSnapshot,
139+
at range: Range<Position>,
140+
buildSettings: SwiftCompileCommand?
141+
) async throws -> [RefactoringEdit] {
142+
guard let swiftLanguageService else {
143+
// `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do.
144+
throw ResponseError.unknown("Connection to the editor closed")
145+
}
146+
let keys = swiftLanguageService.keys
147+
148+
let line = range.lowerBound.line
149+
let utf16Column = range.lowerBound.utf16index
150+
let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column)
151+
let length = snapshot.utf8OffsetRange(of: range).count
152+
153+
let skreq = swiftLanguageService.sourcekitd.dictionary([
154+
keys.request: swiftLanguageService.requests.semanticRefactoring,
155+
// Preferred name for e.g. an extracted variable.
156+
// Empty string means sourcekitd chooses a name automatically.
157+
keys.name: "",
158+
keys.sourceFile: snapshot.uri.sourcekitdSourceFile,
159+
keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath,
160+
// LSP is zero based, but this request is 1 based.
161+
keys.line: line + 1,
162+
keys.column: utf8Column + 1,
163+
keys.length: length,
164+
keys.actionUID: swiftLanguageService.sourcekitd.api.uid_get_from_cstr("source.refactoring.kind.expand.macro")!,
165+
keys.compilerArgs: buildSettings?.compilerArgs as [SKDRequestValue]?,
166+
])
167+
168+
let dict = try await swiftLanguageService.sendSourcekitdRequest(
169+
skreq,
170+
fileContents: snapshot.text
171+
)
172+
guard let expansions = [RefactoringEdit](dict, snapshot, keys) else {
173+
throw SemanticRefactoringError.noEditsNeeded(snapshot.uri)
174+
}
175+
return expansions
176+
}
177+
178+
/// Remove all cached macro expansions for the given primary file, eg. because the macro's plugin might have changed.
179+
func purge(primaryFile: DocumentURI) {
180+
cache.removeAll { $0.snapshotID.uri.primaryFile ?? $0.snapshotID.uri == primaryFile }
181+
}
182+
}
183+
47184
extension SwiftLanguageService {
48185
/// Handles the `ExpandMacroCommand`.
49186
///
@@ -62,23 +199,30 @@ extension SwiftLanguageService {
62199
throw ResponseError.unknown("Connection to the editor closed")
63200
}
64201

65-
guard let primaryFileURL = expandMacroCommand.textDocument.uri.fileURL else {
66-
throw ResponseError.unknown("Given URI is not a file URL")
67-
}
202+
let primaryFileDisplayName =
203+
switch try? ReferenceDocumentURL(from: expandMacroCommand.textDocument.uri) {
204+
case .macroExpansion(let data):
205+
data.bufferName
206+
case nil:
207+
expandMacroCommand.textDocument.uri.fileURL?.lastPathComponent ?? expandMacroCommand.textDocument.uri.pseudoPath
208+
}
68209

69-
let expansion = try await self.refactoring(expandMacroCommand)
210+
let expansions = try await macroExpansionManager.macroExpansions(
211+
in: expandMacroCommand.textDocument.uri,
212+
at: expandMacroCommand.positionRange
213+
)
70214

71215
var completeExpansionFileContent = ""
72216
var completeExpansionDirectoryName = ""
73217

74218
var macroExpansionReferenceDocumentURLs: [ReferenceDocumentURL] = []
75-
for macroEdit in expansion.edits {
219+
for macroEdit in expansions {
76220
if let bufferName = macroEdit.bufferName {
77221
let macroExpansionReferenceDocumentURLData =
78222
ReferenceDocumentURL.macroExpansion(
79223
MacroExpansionReferenceDocumentURLData(
80224
macroExpansionEditRange: macroEdit.range,
81-
primaryFileURL: primaryFileURL,
225+
parent: expandMacroCommand.textDocument.uri,
82226
selectionRange: expandMacroCommand.positionRange,
83227
bufferName: bufferName
84228
)
@@ -90,7 +234,7 @@ extension SwiftLanguageService {
90234

91235
let editContent =
92236
"""
93-
// \(primaryFileURL.lastPathComponent) @ \(macroEdit.range.lowerBound.line + 1):\(macroEdit.range.lowerBound.utf16index + 1) - \(macroEdit.range.upperBound.line + 1):\(macroEdit.range.upperBound.utf16index + 1)
237+
// \(primaryFileDisplayName) @ \(macroEdit.range.lowerBound.line + 1):\(macroEdit.range.lowerBound.utf16index + 1) - \(macroEdit.range.upperBound.line + 1):\(macroEdit.range.upperBound.utf16index + 1)
94238
\(macroEdit.newText)
95239
96240
@@ -154,7 +298,7 @@ extension SwiftLanguageService {
154298
}
155299

156300
completeExpansionFilePath =
157-
completeExpansionFilePath.appendingPathComponent(primaryFileURL.lastPathComponent)
301+
completeExpansionFilePath.appendingPathComponent(primaryFileDisplayName)
158302
do {
159303
try completeExpansionFileContent.write(to: completeExpansionFilePath, atomically: true, encoding: .utf8)
160304
} catch {
@@ -178,23 +322,4 @@ extension SwiftLanguageService {
178322
}
179323
}
180324
}
181-
182-
func expandMacro(macroExpansionURLData: MacroExpansionReferenceDocumentURLData) async throws -> String {
183-
let expandMacroCommand = ExpandMacroCommand(
184-
positionRange: macroExpansionURLData.selectionRange,
185-
textDocument: TextDocumentIdentifier(macroExpansionURLData.primaryFile)
186-
)
187-
188-
let expansion = try await self.refactoring(expandMacroCommand)
189-
190-
guard
191-
let macroExpansionEdit = expansion.edits.filter({
192-
$0.bufferName == macroExpansionURLData.bufferName
193-
}).only
194-
else {
195-
throw ResponseError.unknown("Macro expansion edit doesn't exist")
196-
}
197-
198-
return macroExpansionEdit.newText
199-
}
200325
}

0 commit comments

Comments
 (0)