@@ -14,6 +14,8 @@ import Crypto
14
14
import Foundation
15
15
import LanguageServerProtocol
16
16
import SKLogging
17
+ import SKOptions
18
+ import SKSupport
17
19
import SourceKitD
18
20
19
21
/// Detailed information about the result of a macro expansion operation.
@@ -44,6 +46,141 @@ struct MacroExpansion: RefactoringResponse {
44
46
}
45
47
}
46
48
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
+
47
184
extension SwiftLanguageService {
48
185
/// Handles the `ExpandMacroCommand`.
49
186
///
@@ -62,23 +199,30 @@ extension SwiftLanguageService {
62
199
throw ResponseError . unknown ( " Connection to the editor closed " )
63
200
}
64
201
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
+ }
68
209
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
+ )
70
214
71
215
var completeExpansionFileContent = " "
72
216
var completeExpansionDirectoryName = " "
73
217
74
218
var macroExpansionReferenceDocumentURLs : [ ReferenceDocumentURL ] = [ ]
75
- for macroEdit in expansion . edits {
219
+ for macroEdit in expansions {
76
220
if let bufferName = macroEdit. bufferName {
77
221
let macroExpansionReferenceDocumentURLData =
78
222
ReferenceDocumentURL . macroExpansion (
79
223
MacroExpansionReferenceDocumentURLData (
80
224
macroExpansionEditRange: macroEdit. range,
81
- primaryFileURL : primaryFileURL ,
225
+ parent : expandMacroCommand . textDocument . uri ,
82
226
selectionRange: expandMacroCommand. positionRange,
83
227
bufferName: bufferName
84
228
)
@@ -90,7 +234,7 @@ extension SwiftLanguageService {
90
234
91
235
let editContent =
92
236
"""
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 )
94
238
\( macroEdit. newText)
95
239
96
240
@@ -154,7 +298,7 @@ extension SwiftLanguageService {
154
298
}
155
299
156
300
completeExpansionFilePath =
157
- completeExpansionFilePath. appendingPathComponent ( primaryFileURL . lastPathComponent )
301
+ completeExpansionFilePath. appendingPathComponent ( primaryFileDisplayName )
158
302
do {
159
303
try completeExpansionFileContent. write ( to: completeExpansionFilePath, atomically: true , encoding: . utf8)
160
304
} catch {
@@ -178,23 +322,4 @@ extension SwiftLanguageService {
178
322
}
179
323
}
180
324
}
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
- }
200
325
}
0 commit comments