@@ -14,6 +14,8 @@ import Crypto
1414import Foundation
1515import LanguageServerProtocol
1616import SKLogging
17+ import SKOptions
18+ import SKSupport
1719import 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+
47184extension 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