Skip to content

Commit fd50560

Browse files
committed
Add LSP support for @freestanding Macro Expansions.
------ Simplify `SemanticRefactoring` with new `Refactoring` protocol to handle sourcekitd requests Create and implement `ExpandMacroCommand` while temporarily storing generated expansions. Create test case `testFreestandingMacroExpansion` Manually inject `ExpandMacroCommand` into `retrieveRefactorCodeActions` upon an "Inline Macro" from sourcekitd Address Review Comments Mark `@_spi(Testing) public` for `MacroExpansionEdit` Address Review Comments Create separate directory for each buffer with its name, containing a generated file named as the source file along with position range Fixed generated macro expansion file extension not recognised, by switching to file names which don't contain fragments Address Review Comments Wrap the entire feature under `ExperimentalFeatures` Address Review Comments Make Swift Lint Pass Fix Windows Build not passing
1 parent e5547e9 commit fd50560

17 files changed

+703
-186
lines changed

Sources/LanguageServerProtocol/Requests/ShowDocumentRequest.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
/// Request from the server to the client to show a document on the client side.
14-
///
15-
/// - Parameters:
16-
/// - uri: The uri to show.
17-
/// - external: An optional boolean indicates to show the resource in an external program.
18-
/// - takeFocus: An optional boolean to indicate whether the editor showing the document should take focus or not.
19-
/// - selection: An optional selection range if the document is a text document.
13+
/// Request from the server to the client to show a document on the client
14+
/// side.
2015
public struct ShowDocumentRequest: RequestType {
2116
public static let method: String = "window/showDocument"
2217
public typealias Response = ShowDocumentResponse

Sources/SKCore/ExperimentalFeatures.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ public enum ExperimentalFeature: String, Codable, Sendable, CaseIterable {
1818

1919
/// Add `--experimental-prepare-for-indexing` to the `swift build` command run to prepare a target for indexing.
2020
case swiftpmPrepareForIndexing = "swiftpm-prepare-for-indexing"
21+
22+
/// Enable showing macro expansions via `ShowDocumentRequest`
23+
case showMacroExpansions = "show-macro-expansions"
2124
}

Sources/SKSupport/FileSystem.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ extension AbsolutePath {
3131
}
3232
}
3333

34-
/// The directory to write generated module interfaces
35-
public var defaultDirectoryForGeneratedInterfaces: AbsolutePath {
36-
try! AbsolutePath(validating: NSTemporaryDirectory()).appending(component: "GeneratedInterfaces")
34+
/// The default directory to write generated files
35+
/// `<TEMPORARY_DIRECTORY>/sourcekit-lsp/`
36+
public var defaultDirectoryForGeneratedFiles: AbsolutePath {
37+
try! AbsolutePath(validating: NSTemporaryDirectory()).appending(component: "sourcekit-lsp")
3738
}

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,13 @@ target_sources(SourceKitLSP PRIVATE
4040
Swift/DiagnosticReportManager.swift
4141
Swift/DocumentFormatting.swift
4242
Swift/DocumentSymbols.swift
43+
Swift/ExpandMacroCommand.swift
4344
Swift/FoldingRange.swift
45+
Swift/MacroExpansion.swift
4446
Swift/OpenInterface.swift
47+
Swift/Refactoring.swift
48+
Swift/RefactoringEdit.swift
49+
Swift/RefactorCommand.swift
4550
Swift/RelatedIdentifiers.swift
4651
Swift/RewriteSourceKitPlaceholders.swift
4752
Swift/SemanticRefactorCommand.swift

Sources/SourceKitLSP/SourceKitLSPServer+Options.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,20 @@ extension SourceKitLSPServer {
3838
/// Options for code-completion.
3939
public var completionOptions: SKCompletionOptions
4040

41-
/// Override the default directory where generated interfaces will be stored
42-
public var generatedInterfacesPath: AbsolutePath
41+
/// Override the default directory where generated files will be stored
42+
public var generatedFilesPath: AbsolutePath
43+
44+
/// Path to the generated interfaces
45+
/// `<generatedFilesPath>/GeneratedInterfaces/`
46+
public var generatedInterfacesPath: AbsolutePath {
47+
generatedFilesPath.appending(component: "GeneratedInterfaces")
48+
}
49+
50+
/// Path to the generated macro expansions
51+
/// `<generatedFilesPath>`/GeneratedMacroExpansions/
52+
public var generatedMacroExpansionsPath: AbsolutePath {
53+
generatedFilesPath.appending(component: "GeneratedMacroExpansions")
54+
}
4355

4456
/// The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and
4557
/// sending a `PublishDiagnosticsNotification`.
@@ -64,7 +76,7 @@ extension SourceKitLSPServer {
6476
compilationDatabaseSearchPaths: [RelativePath] = [],
6577
indexOptions: IndexOptions = .init(),
6678
completionOptions: SKCompletionOptions = .init(),
67-
generatedInterfacesPath: AbsolutePath = defaultDirectoryForGeneratedInterfaces,
79+
generatedFilesPath: AbsolutePath = defaultDirectoryForGeneratedFiles,
6880
swiftPublishDiagnosticsDebounceDuration: TimeInterval = 2, /* 2s */
6981
workDoneProgressDebounceDuration: Duration = .seconds(0),
7082
experimentalFeatures: Set<ExperimentalFeature> = [],
@@ -75,7 +87,7 @@ extension SourceKitLSPServer {
7587
self.compilationDatabaseSearchPaths = compilationDatabaseSearchPaths
7688
self.indexOptions = indexOptions
7789
self.completionOptions = completionOptions
78-
self.generatedInterfacesPath = generatedInterfacesPath
90+
self.generatedFilesPath = generatedFilesPath
7991
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
8092
self.experimentalFeatures = experimentalFeatures
8193
self.workDoneProgressDebounceDuration = workDoneProgressDebounceDuration
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
import LanguageServerProtocol
14+
import SourceKitD
15+
16+
public struct ExpandMacroCommand: RefactorCommand {
17+
typealias Response = MacroExpansion
18+
19+
public static let identifier: String = "expand.macro.command"
20+
21+
/// The name of this refactoring action.
22+
public var title = "Expand Macro"
23+
24+
/// The sourcekitd identifier of the refactoring action.
25+
public var actionString = "source.refactoring.kind.expand.macro"
26+
27+
/// The range to expand.
28+
public var positionRange: Range<Position>
29+
30+
/// The text document related to the refactoring action.
31+
public var textDocument: TextDocumentIdentifier
32+
33+
public init(positionRange: Range<Position>, textDocument: TextDocumentIdentifier) {
34+
self.positionRange = positionRange
35+
self.textDocument = textDocument
36+
}
37+
38+
public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
39+
guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue],
40+
case .string(let title)? = dictionary[CodingKeys.title.stringValue],
41+
case .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue],
42+
case .dictionary(let rangeDict)? = dictionary[CodingKeys.positionRange.stringValue]
43+
else {
44+
return nil
45+
}
46+
guard let positionRange = Range<Position>(fromLSPDictionary: rangeDict),
47+
let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict)
48+
else {
49+
return nil
50+
}
51+
52+
self.init(
53+
title: title,
54+
actionString: actionString,
55+
positionRange: positionRange,
56+
textDocument: textDocument
57+
)
58+
}
59+
60+
public init(
61+
title: String,
62+
actionString: String,
63+
positionRange: Range<Position>,
64+
textDocument: TextDocumentIdentifier
65+
) {
66+
self.title = title
67+
self.actionString = actionString
68+
self.positionRange = positionRange
69+
self.textDocument = textDocument
70+
}
71+
72+
public func encodeToLSPAny() -> LSPAny {
73+
return .dictionary([
74+
CodingKeys.title.stringValue: .string(title),
75+
CodingKeys.actionString.stringValue: .string(actionString),
76+
CodingKeys.positionRange.stringValue: positionRange.encodeToLSPAny(),
77+
CodingKeys.textDocument.stringValue: textDocument.encodeToLSPAny(),
78+
])
79+
}
80+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
import Foundation
14+
import LSPLogging
15+
import LanguageServerProtocol
16+
import SourceKitD
17+
18+
/// Detailed information about the result of a macro expansion operation.
19+
///
20+
/// Wraps the information returned by sourcekitd's `semantic_refactoring`
21+
/// request, such as the necessary macro expansion edits.
22+
struct MacroExpansion: RefactoringResponse {
23+
/// The title of the refactoring action.
24+
var title: String
25+
26+
/// The URI of the file where the macro is used
27+
var uri: DocumentURI
28+
29+
/// The resulting array of `RefactoringEdit` of a semantic refactoring request
30+
var edits: [RefactoringEdit]
31+
32+
init(title: String, uri: DocumentURI, refactoringEdits: [RefactoringEdit]) {
33+
self.title = title
34+
self.uri = uri
35+
self.edits = refactoringEdits.compactMap { refactoringEdit in
36+
if refactoringEdit.bufferName == nil && !refactoringEdit.newText.isEmpty {
37+
logger.fault("Unable to retrieve some parts of the expansion")
38+
return nil
39+
}
40+
41+
return refactoringEdit
42+
}
43+
}
44+
}
45+
46+
extension SwiftLanguageService {
47+
/// Handles the `ExpandMacroCommand`.
48+
///
49+
/// Makes a request to sourcekitd and wraps the result into a `MacroExpansion`
50+
/// and then makes a `ShowDocumentRequest` to the client side for each
51+
/// expansion to be displayed.
52+
///
53+
/// - Parameters:
54+
/// - expandMacroCommand: The `ExpandMacroCommand` that triggered this request.
55+
///
56+
/// - Returns: A `[RefactoringEdit]` with the necessary edits and buffer name as a `LSPAny`
57+
func expandMacro(
58+
_ expandMacroCommand: ExpandMacroCommand
59+
) async throws -> LSPAny {
60+
guard let sourceKitLSPServer else {
61+
// `SourceKitLSPServer` has been destructed. We are tearing down the
62+
// language server. Nothing left to do.
63+
throw ResponseError.unknown("Connection to the editor closed")
64+
}
65+
66+
guard let sourceFileURL = expandMacroCommand.textDocument.uri.fileURL else {
67+
throw ResponseError.unknown("Given URI is not a file URL")
68+
}
69+
70+
let expansion = try await self.refactoring(expandMacroCommand)
71+
72+
for macroEdit in expansion.edits {
73+
if let bufferName = macroEdit.bufferName {
74+
// buffer name without ".swift"
75+
let macroExpansionBufferDirectoryName =
76+
bufferName.hasSuffix(".swift")
77+
? String(bufferName.dropLast(6))
78+
: bufferName
79+
80+
let macroExpansionBufferDirectoryURL = self.generatedMacroExpansionsPath
81+
.appendingPathComponent(macroExpansionBufferDirectoryName)
82+
do {
83+
try FileManager.default.createDirectory(
84+
at: macroExpansionBufferDirectoryURL,
85+
withIntermediateDirectories: true
86+
)
87+
} catch {
88+
throw ResponseError.unknown(
89+
"Failed to create directory for macro expansion buffer at path: \(macroExpansionBufferDirectoryURL.path)"
90+
)
91+
}
92+
93+
// name of the source file
94+
let macroExpansionFileName = sourceFileURL.deletingPathExtension().lastPathComponent
95+
96+
// github permalink notation for position range
97+
let macroExpansionPositionRangeIndicator =
98+
"L\(macroEdit.range.lowerBound.line)C\(macroEdit.range.lowerBound.utf16index)-L\(macroEdit.range.upperBound.line)C\(macroEdit.range.upperBound.utf16index)"
99+
100+
let macroExpansionFilePath =
101+
macroExpansionBufferDirectoryURL
102+
.appendingPathComponent(
103+
"\(macroExpansionFileName)_\(macroExpansionPositionRangeIndicator).\(sourceFileURL.pathExtension)"
104+
)
105+
106+
do {
107+
try macroEdit.newText.write(to: macroExpansionFilePath, atomically: true, encoding: .utf8)
108+
} catch {
109+
throw ResponseError.unknown(
110+
"Unable to write macro expansion to file path: \"\(macroExpansionFilePath.path)\""
111+
)
112+
}
113+
114+
Task {
115+
let req = ShowDocumentRequest(uri: DocumentURI(macroExpansionFilePath), selection: macroEdit.range)
116+
117+
let response = await orLog("Sending ShowDocumentRequest to Client") {
118+
try await sourceKitLSPServer.sendRequestToClient(req)
119+
}
120+
121+
if let response, !response.success {
122+
logger.error("client refused to show document for \(expansion.title, privacy: .public)")
123+
}
124+
}
125+
} else if !macroEdit.newText.isEmpty {
126+
logger.fault("Unable to retrieve some parts of macro expansion")
127+
}
128+
}
129+
130+
return expansion.edits.encodeToLSPAny()
131+
}
132+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
import LanguageServerProtocol
14+
import SourceKitD
15+
16+
/// A protocol to be utilised by all commands that are served by sourcekitd refactorings.
17+
protocol RefactorCommand: SwiftCommand {
18+
/// The response type of the refactor command
19+
associatedtype Response: RefactoringResponse
20+
21+
/// The sourcekitd identifier of the refactoring action.
22+
var actionString: String { get set }
23+
24+
/// The range to refactor.
25+
var positionRange: Range<Position> { get set }
26+
27+
/// The text document related to the refactoring action.
28+
var textDocument: TextDocumentIdentifier { get set }
29+
30+
init(title: String, actionString: String, positionRange: Range<Position>, textDocument: TextDocumentIdentifier)
31+
}

0 commit comments

Comments
 (0)