Skip to content

Commit f4c2399

Browse files
committed
Add Semantic Functionality to Macro Expansion Reference Documents (including Nested Macro Expansion)
1 parent a3bb2d7 commit f4c2399

13 files changed

+246
-77
lines changed

Sources/LanguageServerProtocol/SupportTypes/DocumentURI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public struct DocumentURI: Codable, Hashable, Sendable {
7676

7777
public init(_ url: URL) {
7878
self.storage = url
79-
assert(self.storage.scheme != nil, "Received invalid URI without a scheme '\(self.storage.absoluteString)'")
79+
// assert(self.storage.scheme != nil, "Received invalid URI without a scheme '\(self.storage.absoluteString)'")
8080
}
8181

8282
public init(filePath: String, isDirectory: Bool) {

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1674,10 +1674,16 @@ extension SourceKitLSPServer {
16741674
}
16751675

16761676
func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? {
1677-
guard let uri = req.textDocument?.uri else {
1677+
guard var uri = req.textDocument?.uri else {
16781678
logger.error("Attempted to perform executeCommand request without an URL")
16791679
return nil
16801680
}
1681+
1682+
let referenceDocumentURL = try? ReferenceDocumentURL(from: uri)
1683+
if let primaryFileURI = referenceDocumentURL?.primaryFile {
1684+
uri = primaryFileURI
1685+
}
1686+
16811687
guard let workspace = await workspaceForDocument(uri: uri) else {
16821688
throw ResponseError.workspaceNotOpen(uri)
16831689
}

Sources/SourceKitLSP/Swift/CursorInfo.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,10 @@ extension SwiftLanguageService {
158158
keys.cancelOnSubsequentRequest: 0,
159159
keys.offset: offsetRange.lowerBound,
160160
keys.length: offsetRange.upperBound != offsetRange.lowerBound ? offsetRange.count : nil,
161-
keys.sourceFile: snapshot.uri.pseudoPath,
161+
keys.sourceFile: snapshot.uri.actualFilePath,
162+
keys.primaryFile: (try? ReferenceDocumentURL(from: snapshot.uri))?.primaryFile.pseudoPath,
162163
keys.compilerArgs: await self.buildSettings(for: uri)?.compilerArgs as [SKDRequestValue]?,
163164
])
164-
165165
appendAdditionalParameters?(skreq)
166166

167167
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)

Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ actor DiagnosticReportManager {
104104

105105
let skreq = sourcekitd.dictionary([
106106
keys.request: requests.diagnostics,
107-
keys.sourceFile: snapshot.uri.pseudoPath,
107+
keys.sourceFile: snapshot.uri.actualFilePath,
108+
keys.primaryFile: (try? ReferenceDocumentURL(from: snapshot.uri))?.primaryFile.pseudoPath,
108109
keys.compilerArgs: compilerArgs as [SKDRequestValue],
109110
])
110111

Sources/SourceKitLSP/Swift/MacroExpansion.swift

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ struct MacroExpansion: RefactoringResponse {
4444
}
4545
}
4646

47+
actor GeneratedMacroExpansionsStorage {
48+
static var shared = GeneratedMacroExpansionsStorage()
49+
private init() {}
50+
51+
private var generatedMacroExpansions: [String: String] = [:]
52+
53+
func addOrUpdateMacroExpansion(havingBufferName bufferName: String, withContents content: String) {
54+
generatedMacroExpansions[bufferName] = content
55+
}
56+
57+
func retrieveAndDeleteMacroExpansion(havingBufferName bufferName: String) -> String? {
58+
return generatedMacroExpansions.removeValue(forKey: bufferName)
59+
}
60+
}
61+
4762
extension SwiftLanguageService {
4863
/// Handles the `ExpandMacroCommand`.
4964
///
@@ -62,9 +77,9 @@ extension SwiftLanguageService {
6277
throw ResponseError.unknown("Connection to the editor closed")
6378
}
6479

65-
guard let primaryFileURL = expandMacroCommand.textDocument.uri.fileURL else {
66-
throw ResponseError.unknown("Given URI is not a file URL")
67-
}
80+
var primaryFileURL = expandMacroCommand.textDocument.uri.arbitrarySchemeURL
81+
let referenceDocumentURL = try? ReferenceDocumentURL(from: expandMacroCommand.textDocument.uri)
82+
primaryFileURL = referenceDocumentURL?.primaryFile.arbitrarySchemeURL ?? primaryFileURL
6883

6984
let expansion = try await self.refactoring(expandMacroCommand)
7085

@@ -74,15 +89,32 @@ extension SwiftLanguageService {
7489
var macroExpansionReferenceDocumentURLs: [ReferenceDocumentURL] = []
7590
for macroEdit in expansion.edits {
7691
if let bufferName = macroEdit.bufferName {
92+
await GeneratedMacroExpansionsStorage.shared.addOrUpdateMacroExpansion(
93+
havingBufferName: bufferName,
94+
withContents: macroEdit.newText
95+
)
96+
7797
let macroExpansionReferenceDocumentURLData =
78-
ReferenceDocumentURL.macroExpansion(
79-
MacroExpansionReferenceDocumentURLData(
80-
macroExpansionEditRange: macroEdit.range,
81-
primaryFileURL: primaryFileURL,
82-
selectionRange: expandMacroCommand.positionRange,
83-
bufferName: bufferName
98+
switch referenceDocumentURL {
99+
case .macroExpansion(let parentReferenceDocument):
100+
ReferenceDocumentURL.macroExpansion(
101+
MacroExpansionReferenceDocumentURLData(
102+
macroExpansionEditRange: macroEdit.range,
103+
primaryFileURL: primaryFileURL,
104+
selectionRange: expandMacroCommand.positionRange,
105+
bufferName: bufferName
106+
)
84107
)
85-
)
108+
case nil:
109+
ReferenceDocumentURL.macroExpansion(
110+
MacroExpansionReferenceDocumentURLData(
111+
macroExpansionEditRange: macroEdit.range,
112+
primaryFileURL: primaryFileURL,
113+
selectionRange: expandMacroCommand.positionRange,
114+
bufferName: bufferName
115+
)
116+
)
117+
}
86118

87119
macroExpansionReferenceDocumentURLs.append(macroExpansionReferenceDocumentURLData)
88120

@@ -108,10 +140,19 @@ extension SwiftLanguageService {
108140
let expansionURIs = try macroExpansionReferenceDocumentURLs.map {
109141
return DocumentURI(try $0.url)
110142
}
143+
111144
Task {
145+
var uri = expandMacroCommand.textDocument.uri
146+
var position = expandMacroCommand.positionRange.lowerBound
147+
148+
if let referenceDocumentURL, case let .macroExpansion(referenceDocumentURLData) = referenceDocumentURL {
149+
uri = referenceDocumentURL.primaryFile
150+
position = referenceDocumentURLData.macroExpansionEditRange.lowerBound
151+
}
152+
112153
let req = PeekDocumentsRequest(
113-
uri: expandMacroCommand.textDocument.uri,
114-
position: expandMacroCommand.positionRange.lowerBound,
154+
uri: uri,
155+
position: position,
115156
locations: expansionURIs
116157
)
117158

@@ -180,21 +221,14 @@ extension SwiftLanguageService {
180221
}
181222

182223
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-
190224
guard
191-
let macroExpansionEdit = expansion.edits.filter({
192-
$0.bufferName == macroExpansionURLData.bufferName
193-
}).only
225+
let content = await GeneratedMacroExpansionsStorage.shared.retrieveAndDeleteMacroExpansion(
226+
havingBufferName: macroExpansionURLData.bufferName
227+
)
194228
else {
195229
throw ResponseError.unknown("Macro expansion edit doesn't exist")
196230
}
197231

198-
return macroExpansionEdit.newText
232+
return content
199233
}
200234
}

Sources/SourceKitLSP/Swift/MacroExpansionReferenceDocumentURLData.swift

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,16 @@ package struct MacroExpansionReferenceDocumentURLData {
4848
}
4949

5050
package var queryItems: [URLQueryItem] {
51-
[
52-
URLQueryItem(name: Parameters.primaryFilePath, value: primaryFileURL.path(percentEncoded: false)),
51+
var queryItems = [
5352
URLQueryItem(name: Parameters.fromLine, value: String(selectionRange.lowerBound.line)),
5453
URLQueryItem(name: Parameters.fromColumn, value: String(selectionRange.lowerBound.utf16index)),
5554
URLQueryItem(name: Parameters.toLine, value: String(selectionRange.upperBound.line)),
5655
URLQueryItem(name: Parameters.toColumn, value: String(selectionRange.upperBound.utf16index)),
5756
URLQueryItem(name: Parameters.bufferName, value: bufferName),
57+
URLQueryItem(name: Parameters.primaryFilePath, value: primaryFileURL.path(percentEncoded: false)),
5858
]
59+
60+
return queryItems
5961
}
6062

6163
package init(displayName: String, queryItems: [URLQueryItem]) throws {
@@ -71,7 +73,7 @@ package struct MacroExpansionReferenceDocumentURLData {
7173

7274
guard let primaryFileURL = URL(string: "file://\(primaryFilePath)") else {
7375
throw ReferenceDocumentURLError(
74-
description: "Unable to parse source file url"
76+
description: "Unable to parse primary file url"
7577
)
7678
}
7779

@@ -82,6 +84,61 @@ package struct MacroExpansionReferenceDocumentURLData {
8284
self.macroExpansionEditRange = try Self.parse(displayName: displayName)
8385
}
8486

87+
/// The file path of the document that originally contains the contents of this reference document
88+
/// since `sourcekitd` cannot understand reference document urls.
89+
///
90+
/// For any `ReferenceDocumentURL.macroExpansion`, its `actualFilePath` will be its sourcekitd `bufferName`
91+
///
92+
/// *Example:*
93+
///
94+
/// User's source File:
95+
/// URL: `file:///path/to/swift_file.swift`
96+
/// ```swift
97+
/// let a = 10
98+
/// let b = 5
99+
/// print(#stringify(a + b))
100+
/// ```
101+
///
102+
/// Generated content of reference document url:
103+
/// URL:
104+
/// `sourcekit-lsp://swift-macro-expansion/L3C7-L3C23.swift?primaryFilePath=/path/to/swift_file.swift&fromLine=3&fromColumn=8&toLine=3&toColumn=8&bufferName=@__swift_macro_..._Stringify_.swift`
105+
/// ```swift
106+
/// (a + b, "a + b")
107+
/// ```
108+
///
109+
/// Here the `actualFilePath` of the reference document url is `@__swift_macro_..._Stringify_.swift`
110+
///
111+
/// *NOTE*: In case of nested macro expansion reference documents, the `actualFilePath` will be their corresponding
112+
/// `bufferName`s
113+
package var actualFilePath: String {
114+
bufferName
115+
}
116+
117+
/// The URI of the document from which this reference document was derived.
118+
/// This is used to determine the workspace and language service that is used to generate the reference document.
119+
///
120+
/// *Example:*
121+
///
122+
/// User's source File:
123+
/// URL: `file://path/to/swift_file.swift`
124+
/// ```swift
125+
/// let a = 10
126+
/// let b = 5
127+
/// print(#stringify(a + b))
128+
/// ```
129+
///
130+
/// Generated content of reference document url:
131+
/// URL:
132+
/// `sourcekit-lsp://swift-macro-expansion/L3C7-L3C23.swift?primaryFilePath=/path/to/swift_file.swift&fromLine=3&fromColumn=8&toLine=3&toColumn=8&bufferName=@__swift_macro_..._Stringify_.swift`
133+
/// ```swift
134+
/// (a + b, "a + b")
135+
/// ```
136+
///
137+
/// Here the `primaryFile` of the reference document url is a `DocumentURI`
138+
/// with the following url: `file:///path/to/swift_file.swift`
139+
///
140+
/// *NOTE*: In case of nested macro expansion reference documents, they all will have the same `primaryFile`
141+
/// as that of the first macro expansion reference document i.e. `primaryFile` doesn't change.
85142
package var primaryFile: DocumentURI {
86143
DocumentURI(primaryFileURL)
87144
}

Sources/SourceKitLSP/Swift/OpenInterface.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,20 @@ extension SwiftLanguageService {
4444
symbol: symbol
4545
)
4646
} else {
47+
let primaryDocument =
48+
if let referenceDocument = try? ReferenceDocumentURL(from: document) {
49+
referenceDocument.primaryFile
50+
} else {
51+
document
52+
}
53+
4754
let interfaceInfo = try await self.generatedInterfaceInfo(
48-
document: document,
55+
document: primaryDocument,
4956
moduleName: moduleName,
5057
groupName: groupName,
5158
interfaceURI: interfaceDocURI
5259
)
60+
5361
try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8)
5462
let snapshot = DocumentSnapshot(
5563
uri: interfaceDocURI,

Sources/SourceKitLSP/Swift/RefactoringResponse.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import LanguageServerProtocol
1414
import SKLogging
15+
import SKSupport
1516
import SourceKitD
1617

1718
protocol RefactoringResponse {
@@ -102,28 +103,35 @@ extension SwiftLanguageService {
102103

103104
let uri = refactorCommand.textDocument.uri
104105
let snapshot = try self.documentManager.latestSnapshot(uri)
106+
let referenceDocument = try? ReferenceDocumentURL(from: snapshot.uri)
107+
105108
let line = refactorCommand.positionRange.lowerBound.line
106109
let utf16Column = refactorCommand.positionRange.lowerBound.utf16index
107110
let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column)
111+
let length = snapshot.utf8OffsetRange(of: refactorCommand.positionRange).count
112+
let buildSettings =
113+
await self.buildSettings(for: referenceDocument?.primaryFile ?? snapshot.uri)?.compilerArgs as [SKDRequestValue]?
108114

109115
let skreq = sourcekitd.dictionary([
110116
keys.request: self.requests.semanticRefactoring,
111117
// Preferred name for e.g. an extracted variable.
112118
// Empty string means sourcekitd chooses a name automatically.
113119
keys.name: "",
114-
keys.sourceFile: uri.pseudoPath,
120+
keys.sourceFile: uri.actualFilePath,
121+
keys.primaryFile: referenceDocument?.primaryFile.pseudoPath,
115122
// LSP is zero based, but this request is 1 based.
116123
keys.line: line + 1,
117124
keys.column: utf8Column + 1,
118-
keys.length: snapshot.utf8OffsetRange(of: refactorCommand.positionRange).count,
125+
keys.length: length,
119126
keys.actionUID: self.sourcekitd.api.uid_get_from_cstr(refactorCommand.actionString)!,
120-
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
127+
keys.compilerArgs: buildSettings,
121128
])
122129

123130
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
124131
guard let refactor = T.Response(refactorCommand.title, dict, snapshot, self.keys) else {
125132
throw SemanticRefactoringError.noEditsNeeded(uri)
126133
}
134+
127135
return refactor
128136
}
129137
}

Sources/SourceKitLSP/Swift/ReferenceDocumentURL.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ package enum ReferenceDocumentURL {
6363

6464
switch documentType {
6565
case MacroExpansionReferenceDocumentURLData.documentType:
66-
guard let queryItems = URLComponents(string: url.absoluteString)?.queryItems else {
66+
guard
67+
let queryItems = URLComponents(string: url.absoluteString.removingPercentEncoding ?? url.absoluteString)?
68+
.queryItems
69+
else {
6770
throw ReferenceDocumentURLError(
6871
description: "No queryItems passed for macro expansion reference document: \(url)"
6972
)
@@ -85,8 +88,17 @@ package enum ReferenceDocumentURL {
8588
}
8689
}
8790

88-
/// The URI of the document from which this reference document was derived. This is used to determine the
89-
/// workspace and language service that is used to generate the reference document.
91+
/// The file path of the document that originally contains the contents of this reference document
92+
/// since sourcekitd cannot understand reference document urls.
93+
var actualFilePath: String {
94+
switch self {
95+
case let .macroExpansion(data):
96+
data.actualFilePath
97+
}
98+
}
99+
100+
/// The URI of the source file from which this reference document was derived.
101+
/// This is used to determine the workspace and language service that is used to generate the reference document.
90102
var primaryFile: DocumentURI {
91103
switch self {
92104
case let .macroExpansion(data):
@@ -95,6 +107,20 @@ package enum ReferenceDocumentURL {
95107
}
96108
}
97109

110+
extension DocumentURI {
111+
/// The file path of the document that originally contains the contents of the reference document
112+
/// of this `DocumentURI` since sourcekitd cannot understand reference document urls.
113+
///
114+
/// If the `DocumentURI` is not a reference document, this gives the `pseudoPath`
115+
var actualFilePath: String {
116+
if let referenceDocument = try? ReferenceDocumentURL(from: self) {
117+
referenceDocument.actualFilePath
118+
} else {
119+
self.pseudoPath
120+
}
121+
}
122+
}
123+
98124
package struct ReferenceDocumentURLError: Error, CustomStringConvertible {
99125
package var description: String
100126

Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ extension SwiftLanguageService {
6969
keys.request: requests.relatedIdents,
7070
keys.cancelOnSubsequentRequest: 0,
7171
keys.offset: snapshot.utf8Offset(of: position),
72-
keys.sourceFile: snapshot.uri.pseudoPath,
72+
keys.sourceFile: snapshot.uri.actualFilePath,
73+
keys.primaryFile: (try? ReferenceDocumentURL(from: snapshot.uri))?.primaryFile.pseudoPath,
7374
keys.includeNonEditableBaseNames: includeNonEditableBaseNames ? 1 : 0,
7475
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
7576
])

0 commit comments

Comments
 (0)