Skip to content

Commit c9e559b

Browse files
committed
Allow macro expansions to be viewed through GetReferenceDocumentRequest instead of storing in temporary files
1 parent 1dfda5b commit c9e559b

File tree

10 files changed

+226
-51
lines changed

10 files changed

+226
-51
lines changed

Documentation/LSP Extensions.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,28 @@ export interface PeekDocumentsResult {
473473
success: boolean;
474474
}
475475
```
476+
477+
## `workspace/getReferenceDocument`
478+
479+
Request from the client to the server asking for contents of a URI having a custom scheme.
480+
For example: "swift-macro-expansion://"
481+
482+
- params: `GetReferenceDocumentParams`
483+
484+
- result: `GetReferenceDocumentResponse`
485+
486+
```ts
487+
export interface GetReferenceDocumentParams {
488+
/**
489+
* The `DocumentUri` of the custom scheme url for which content is required
490+
*/
491+
uri: langclient.DocumentUri;
492+
}
493+
494+
/**
495+
* Response containing `content` of `GetReferenceDocumentRequest`
496+
*/
497+
export interface GetReferenceDocumentResult {
498+
content: string;
499+
}
500+
```

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ add_library(LanguageServerProtocol STATIC
5555
Requests/ExecuteCommandRequest.swift
5656
Requests/FoldingRangeRequest.swift
5757
Requests/FormattingRequests.swift
58+
Requests/GetReferenceDocumentRequest.swift
5859
Requests/HoverRequest.swift
5960
Requests/ImplementationRequest.swift
6061
Requests/IndexedRenameRequest.swift

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public let builtinRequests: [_RequestType.Type] = [
4848
DocumentTestsRequest.self,
4949
ExecuteCommandRequest.self,
5050
FoldingRangeRequest.self,
51+
GetReferenceDocumentRequest.self,
5152
HoverRequest.self,
5253
ImplementationRequest.self,
5354
InitializeRequest.self,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Request from the client to the server asking for contents of a URI having a custom scheme **(LSP Extension)**
14+
/// For example: "swift-macro-expansion://"
15+
///
16+
/// - Parameters:
17+
/// - uri: The `DocumentUri` of the custom scheme url for which content is required
18+
///
19+
/// - Returns: `GetReferenceDocumentResponse` which contains the `content` to be displayed.
20+
///
21+
/// ### LSP Extension
22+
///
23+
/// This request is an extension to LSP supported by SourceKit-LSP.
24+
public struct GetReferenceDocumentRequest: RequestType {
25+
public static let method: String = "workspace/getReferenceDocument"
26+
public typealias Response = GetReferenceDocumentResponse
27+
28+
public var uri: DocumentURI
29+
30+
public init(uri: DocumentURI) {
31+
self.uri = uri
32+
}
33+
}
34+
35+
/// Response containing `content` of `GetReferenceDocumentRequest`
36+
public struct GetReferenceDocumentResponse: ResponseType {
37+
public var content: String
38+
39+
public init(content: String) {
40+
self.content = content
41+
}
42+
}

Sources/SourceKitLSP/Clang/ClangLanguageService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,10 @@ extension ClangLanguageService {
647647
func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? {
648648
return try await forwardRequestToClangd(req)
649649
}
650+
651+
func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse {
652+
throw ResponseError.unknown("unsupported method")
653+
}
650654
}
651655

652656
/// Clang build settings derived from a `FileBuildSettingsChange`.

Sources/SourceKitLSP/LanguageService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ public protocol LanguageService: AnyObject, Sendable {
250250

251251
func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny?
252252

253+
func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse
254+
253255
/// Perform a syntactic scan of the file at the given URI for test cases and test classes.
254256
///
255257
/// This is used as a fallback to show the test cases in a file if the index for a given file is not up-to-date.

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,8 @@ extension SourceKitLSPServer: MessageHandler {
731731
await request.reply { try await executeCommand(request.params) }
732732
case let request as RequestAndReply<FoldingRangeRequest>:
733733
await self.handleRequest(for: request, requestHandler: self.foldingRange)
734+
case let request as RequestAndReply<GetReferenceDocumentRequest>:
735+
await request.reply { try await getReferenceDocument(request.params) }
734736
case let request as RequestAndReply<HoverRequest>:
735737
await self.handleRequest(for: request, requestHandler: self.hover)
736738
case let request as RequestAndReply<ImplementationRequest>:
@@ -1633,6 +1635,25 @@ extension SourceKitLSPServer {
16331635
return try await languageService.executeCommand(executeCommand)
16341636
}
16351637

1638+
func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse {
1639+
guard let givenURL = URL(string: req.uri.stringValue) else {
1640+
throw ResponseError.unknown("Invalid URL")
1641+
}
1642+
1643+
let filePath = "file:/" + givenURL.pathComponents.dropLast().joined(separator: "/")
1644+
let sourceFileURI = try DocumentURI(string: filePath)
1645+
1646+
guard let workspace = await workspaceForDocument(uri: sourceFileURI) else {
1647+
throw ResponseError.workspaceNotOpen(sourceFileURI)
1648+
}
1649+
1650+
guard let languageService = workspace.documentService.value[sourceFileURI] else {
1651+
throw ResponseError.unknown("No Language Service for URI: \(sourceFileURI)")
1652+
}
1653+
1654+
return try await languageService.getReferenceDocument(req)
1655+
}
1656+
16361657
func codeAction(
16371658
_ req: CodeActionRequest,
16381659
workspace: Workspace,

Sources/SourceKitLSP/Swift/MacroExpansion.swift

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -74,50 +74,38 @@ extension SwiftLanguageService {
7474
var macroExpansionFilePaths: [URL] = []
7575
for macroEdit in expansion.edits {
7676
if let bufferName = macroEdit.bufferName {
77-
// buffer name without ".swift"
78-
let macroExpansionBufferDirectoryName =
79-
bufferName.hasSuffix(".swift")
80-
? String(bufferName.dropLast(6))
81-
: bufferName
82-
83-
let macroExpansionBufferDirectoryURL = self.generatedMacroExpansionsPath
84-
.appendingPathComponent(macroExpansionBufferDirectoryName)
85-
86-
completeExpansionDirectoryName += "\(bufferName)-"
87-
88-
do {
89-
try FileManager.default.createDirectory(
90-
at: macroExpansionBufferDirectoryURL,
91-
withIntermediateDirectories: true
92-
)
93-
} catch {
94-
throw ResponseError.unknown(
95-
"Failed to create directory for macro expansion buffer at path: \(macroExpansionBufferDirectoryURL.path)"
96-
)
77+
// Custom Scheme for Macro Expansions:
78+
// `swift-macro-expansion://[path_to_source_file]/LaCb-LcCd.swift?lowerL=&lowerC=&upperL=&upperC=&bufferName=`
79+
// --------------------- --------------------- --------------- ------------------------------- -----------
80+
// 1 2 3 4 5
81+
//
82+
// 1. Scheme
83+
// 2. The path to the source file
84+
// 3. Position range of macro after expansion (used for display purpose in VS Code)
85+
// 4. Selection range of the cursor
86+
// 5. Buffer Name of the required Macro Expansion
87+
88+
guard var components = URLComponents(string: sourceFileURL.absoluteString) else {
89+
throw ResponseError.unknown("Invalid URL")
9790
}
98-
99-
// name of the source file
100-
let macroExpansionFileName = sourceFileURL.deletingPathExtension().lastPathComponent
101-
102-
// github permalink notation for position range
103-
let macroExpansionPositionRangeIndicator =
104-
"L\(macroEdit.range.lowerBound.line + 1)C\(macroEdit.range.lowerBound.utf16index + 1)-L\(macroEdit.range.upperBound.line + 1)C\(macroEdit.range.upperBound.utf16index + 1)"
105-
106-
let macroExpansionFilePath =
107-
macroExpansionBufferDirectoryURL
108-
.appendingPathComponent(
109-
"\(macroExpansionFileName)_\(macroExpansionPositionRangeIndicator).\(sourceFileURL.pathExtension)"
110-
)
111-
112-
do {
113-
try macroEdit.newText.write(to: macroExpansionFilePath, atomically: true, encoding: .utf8)
114-
} catch {
115-
throw ResponseError.unknown(
116-
"Unable to write macro expansion to file path: \"\(macroExpansionFilePath.path)\""
117-
)
91+
components.scheme = "swift-macro-expansion"
92+
components.path +=
93+
"/L\(macroEdit.range.lowerBound.line + 1)C\(macroEdit.range.lowerBound.utf16index + 1)-L\(macroEdit.range.upperBound.line + 1)C\(macroEdit.range.upperBound.utf16index + 1).swift"
94+
let queryItems = [
95+
URLQueryItem(name: "lowerL", value: String(expandMacroCommand.positionRange.lowerBound.line)),
96+
URLQueryItem(name: "lowerC", value: String(expandMacroCommand.positionRange.lowerBound.utf16index)),
97+
URLQueryItem(name: "upperL", value: String(expandMacroCommand.positionRange.upperBound.line)),
98+
URLQueryItem(name: "upperC", value: String(expandMacroCommand.positionRange.upperBound.utf16index)),
99+
URLQueryItem(name: "bufferName", value: bufferName),
100+
]
101+
components.queryItems = queryItems
102+
103+
guard let url = components.url else {
104+
throw ResponseError.unknown("Failed to construct URL")
118105
}
106+
macroExpansionFilePaths.append(url)
119107

120-
macroExpansionFilePaths.append(macroExpansionFilePath)
108+
completeExpansionDirectoryName += "\(bufferName)-"
121109

122110
let editContent =
123111
"""
@@ -208,4 +196,29 @@ extension SwiftLanguageService {
208196
}
209197
}
210198
}
199+
200+
func expandMacro(uri: DocumentURI, selectionRange: Range<Position>, bufferName: String) async throws -> String {
201+
guard let sourceKitLSPServer else {
202+
// `SourceKitLSPServer` has been destructed. We are tearing down the
203+
// language server. Nothing left to do.
204+
throw ResponseError.unknown("Connection to the editor closed")
205+
}
206+
207+
let expandMacroCommand = ExpandMacroCommand(
208+
positionRange: selectionRange,
209+
textDocument: TextDocumentIdentifier(uri)
210+
)
211+
212+
let expansion = try await self.refactoring(expandMacroCommand)
213+
214+
let macroExpansionEdit = expansion.edits.filter {
215+
$0.bufferName == bufferName
216+
}.only
217+
218+
guard let content = macroExpansionEdit?.newText else {
219+
throw ResponseError.unknown("Macro Expansion Edit doesn't Exist")
220+
}
221+
222+
return content
223+
}
211224
}

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,48 @@ extension SwiftLanguageService {
968968

969969
return nil
970970
}
971+
972+
public func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse {
973+
switch req.uri.scheme {
974+
case "swift-macro-expansion":
975+
guard let givenURL = URL(string: req.uri.stringValue) else {
976+
throw ResponseError.unknown("Unable to parse given URL")
977+
}
978+
979+
let filePath = "file:/" + givenURL.pathComponents.dropLast().joined(separator: "/")
980+
981+
guard let sourceFileURL = try DocumentURI(string: filePath).fileURL else {
982+
throw ResponseError.unknown("Unable to obtain sourceFileURL")
983+
}
984+
985+
guard let components = URLComponents(string: givenURL.absoluteString) else {
986+
throw ResponseError.unknown("Unable to obtain components of url")
987+
}
988+
989+
guard let lowerL = Int(components.queryItems?.first { $0.name == "lowerL" }?.value ?? ""),
990+
let lowerC = Int(components.queryItems?.first { $0.name == "lowerC" }?.value ?? ""),
991+
let upperL = Int(components.queryItems?.first { $0.name == "upperL" }?.value ?? ""),
992+
let upperC = Int(components.queryItems?.first { $0.name == "upperC" }?.value ?? ""),
993+
let bufferName = components.queryItems?.first { $0.name == "bufferName" }?.value
994+
else {
995+
throw ResponseError.unknown("Invalid query in URL")
996+
}
997+
998+
let selectionRange = Position(line: lowerL, utf16index: lowerC)..<Position(line: upperL, utf16index: upperC)
999+
1000+
let content = try await expandMacro(
1001+
uri: DocumentURI(sourceFileURL),
1002+
selectionRange: selectionRange,
1003+
bufferName: bufferName
1004+
)
1005+
1006+
return GetReferenceDocumentResponse(content: content)
1007+
default:
1008+
throw ResponseError.unknown("Invalid Scheme")
1009+
}
1010+
1011+
throw ResponseError.unknown("No Content to show")
1012+
}
9711013
}
9721014

9731015
extension SwiftLanguageService: SKDNotificationHandler {

Tests/SourceKitLSPTests/ExecuteCommandTests.swift

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -244,27 +244,39 @@ final class ExecuteCommandTests: XCTestCase {
244244

245245
try await fulfillmentOfOrThrow([expectation])
246246

247-
let urls = try XCTUnwrap(
247+
let urlStrings = try XCTUnwrap(
248248
peekDocumentsRequestURIs.value?.map {
249249
return try XCTUnwrap(
250-
$0.fileURL,
250+
$0.stringValue,
251251
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
252252
)
253253
},
254254
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
255255
)
256256

257-
let filesContents = try urls.map { try String(contentsOf: $0, encoding: .utf8) }
257+
var filesContents = [String]()
258+
for url in urlStrings {
259+
let result = try await project.testClient.send(GetReferenceDocumentRequest(uri: DocumentURI(string: url)))
260+
261+
filesContents.append(result.content)
262+
}
258263

259264
XCTAssertEqual(
260265
filesContents.only,
261266
"(1 + 2, \"1 + 2\")",
262267
"File doesn't contain macro expansion. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
263268
)
264269

270+
let urls = try urlStrings.map {
271+
try XCTUnwrap(
272+
URL(string: $0),
273+
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
274+
)
275+
}
276+
265277
XCTAssertEqual(
266278
urls.only?.lastPathComponent,
267-
"MyMacroClient_L5C3-L5C20.swift",
279+
"L5C3-L5C20.swift",
268280
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
269281
)
270282
} else {
@@ -417,17 +429,22 @@ final class ExecuteCommandTests: XCTestCase {
417429

418430
try await fulfillmentOfOrThrow([expectation])
419431

420-
let urls = try XCTUnwrap(
432+
let urlStrings = try XCTUnwrap(
421433
peekDocumentsRequestURIs.value?.map {
422434
return try XCTUnwrap(
423-
$0.fileURL,
435+
$0.stringValue,
424436
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
425437
)
426438
},
427439
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
428440
)
429441

430-
let filesContents = try urls.map { try String(contentsOf: $0, encoding: .utf8) }
442+
var filesContents = [String]()
443+
for url in urlStrings {
444+
let result = try await project.testClient.send(GetReferenceDocumentRequest(uri: DocumentURI(string: url)))
445+
446+
filesContents.append(result.content)
447+
}
431448

432449
XCTAssertEqual(
433450
filesContents,
@@ -439,12 +456,19 @@ final class ExecuteCommandTests: XCTestCase {
439456
"Files doesn't contain correct macro expansion. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
440457
)
441458

459+
let urls = try urlStrings.map {
460+
try XCTUnwrap(
461+
URL(string: $0),
462+
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
463+
)
464+
}
465+
442466
XCTAssertEqual(
443467
urls.map { $0.lastPathComponent },
444468
[
445-
"MyMacroClient_L7C3-L7C3.swift",
446-
"MyMacroClient_L8C3-L8C3.swift",
447-
"MyMacroClient_L9C1-L9C1.swift",
469+
"L7C3-L7C3.swift",
470+
"L8C3-L8C3.swift",
471+
"L9C1-L9C1.swift",
448472
],
449473
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
450474
)

0 commit comments

Comments
 (0)