Skip to content

Commit 1d5f66e

Browse files
committed
Create and implement ExpandMacroCommand while temporarily storing generated expansions.
1 parent 22544ff commit 1d5f66e

File tree

8 files changed

+264
-23
lines changed

8 files changed

+264
-23
lines changed

Sources/SKSupport/FileSystem.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,8 @@ extension AbsolutePath {
3535
public var defaultDirectoryForGeneratedInterfaces: AbsolutePath {
3636
try! AbsolutePath(validating: NSTemporaryDirectory()).appending(component: "GeneratedInterfaces")
3737
}
38+
39+
/// The directory to write generated macro expansions
40+
public var defaultDirectoryForGeneratedMacroExpansions: AbsolutePath {
41+
try! AbsolutePath(validating: NSTemporaryDirectory()).appending(component: "GeneratedMacroExpansions")
42+
}

Sources/SourceKitLSP/SourceKitLSPServer+Options.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ extension SourceKitLSPServer {
4141
/// Override the default directory where generated interfaces will be stored
4242
public var generatedInterfacesPath: AbsolutePath
4343

44+
/// Override the default directory where generated macro expansions will be stored
45+
public var generatedMacroExpansionsPath: AbsolutePath
46+
4447
/// The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and
4548
/// sending a `PublishDiagnosticsNotification`.
4649
///
@@ -60,6 +63,7 @@ extension SourceKitLSPServer {
6063
indexOptions: IndexOptions = .init(),
6164
completionOptions: SKCompletionOptions = .init(),
6265
generatedInterfacesPath: AbsolutePath = defaultDirectoryForGeneratedInterfaces,
66+
generatedMacroExpansionsPath: AbsolutePath = defaultDirectoryForGeneratedMacroExpansions,
6367
swiftPublishDiagnosticsDebounceDuration: TimeInterval = 2, /* 2s */
6468
experimentalFeatures: Set<ExperimentalFeature> = [],
6569
indexTestHooks: IndexTestHooks = IndexTestHooks()
@@ -70,6 +74,7 @@ extension SourceKitLSPServer {
7074
self.indexOptions = indexOptions
7175
self.completionOptions = completionOptions
7276
self.generatedInterfacesPath = generatedInterfacesPath
77+
self.generatedMacroExpansionsPath = generatedMacroExpansionsPath
7378
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
7479
self.experimentalFeatures = experimentalFeatures
7580
self.indexTestHooks = indexTestHooks
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
public static let identifier: String = "expand.macro.command"
18+
19+
/// The name of this refactoring action.
20+
public var title = "Expand Macro"
21+
22+
/// The sourcekitd identifier of the refactoring action.
23+
public var actionString = "source.refactoring.kind.expand.macro"
24+
25+
/// The range to expand.
26+
public var positionRange: Range<Position>
27+
28+
/// The text document related to the refactoring action.
29+
public var textDocument: TextDocumentIdentifier
30+
31+
public init(positionRange: Range<Position>, textDocument: TextDocumentIdentifier) {
32+
self.positionRange = positionRange
33+
self.textDocument = textDocument
34+
}
35+
36+
public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
37+
guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue],
38+
case .string(let title)? = dictionary[CodingKeys.title.stringValue],
39+
case .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue],
40+
case .dictionary(let rangeDict)? = dictionary[CodingKeys.positionRange.stringValue]
41+
else {
42+
return nil
43+
}
44+
guard let positionRange = Range<Position>(fromLSPDictionary: rangeDict),
45+
let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict)
46+
else {
47+
return nil
48+
}
49+
50+
self.init(
51+
title: title,
52+
actionString: actionString,
53+
positionRange: positionRange,
54+
textDocument: textDocument
55+
)
56+
}
57+
58+
public init(title: String, actionString: String, positionRange: Range<Position>, textDocument: TextDocumentIdentifier)
59+
{
60+
self.title = title
61+
self.actionString = actionString
62+
self.positionRange = positionRange
63+
self.textDocument = textDocument
64+
}
65+
66+
public func encodeToLSPAny() -> LSPAny {
67+
return .dictionary([
68+
CodingKeys.title.stringValue: .string(title),
69+
CodingKeys.actionString.stringValue: .string(actionString),
70+
CodingKeys.positionRange.stringValue: positionRange.encodeToLSPAny(),
71+
CodingKeys.textDocument.stringValue: textDocument.encodeToLSPAny(),
72+
])
73+
}
74+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 LSPLogging
14+
import LanguageServerProtocol
15+
import SourceKitD
16+
17+
/// Detailed information about the result of a macro expansion operation.
18+
///
19+
/// Wraps the information returned by sourcekitd's `semantic_refactoring` request, such as the necessary macro expansion edits.
20+
struct MacroExpansion: Refactoring {
21+
22+
/// The title of the refactoring action.
23+
var title: String
24+
25+
/// The URI of the file where the macro is used
26+
var uri: DocumentURI
27+
28+
/// The resulting array of `MacroExpansionEdit` of a semantic refactoring request
29+
var edits: [MacroExpansionEdit]
30+
31+
init(title: String, uri: DocumentURI, refactoringEdits: [RefactoringEdit]) {
32+
self.title = title
33+
self.uri = uri
34+
self.edits = refactoringEdits.map { refactoringEdit in
35+
MacroExpansionEdit(
36+
range: refactoringEdit.startPosition..<refactoringEdit.endPosition,
37+
newText: refactoringEdit.newText,
38+
bufferName: refactoringEdit.bufferName ?? "" // TODO: handle bufferName if nil
39+
)
40+
}
41+
}
42+
43+
}
44+
45+
extension SwiftLanguageService {
46+
/// Handles the `ExpandMacroCommand`.
47+
///
48+
/// Makes request to sourcekitd and wraps the result into a `MacroExpansion` and then makes `ShowDocumentRequest` to the client side for each expansion to be displayed.
49+
///
50+
/// - Parameters:
51+
/// - refactorCommand: The expand macro `SwiftCommand` that triggered this request.
52+
///
53+
/// - Returns:
54+
/// - an `[MacroExpansionEdit]` with the necessary edits and buffer name as a `LSPAny`
55+
func expandMacro(
56+
_ refactorCommand: any SwiftCommand
57+
) async throws -> LSPAny {
58+
guard let sourceKitLSPServer else {
59+
// `SourceKitLSPServer` has been destructed. We are tearing down the language
60+
// server. Nothing left to do.
61+
throw ResponseError.unknown("Connection to the editor closed")
62+
}
63+
64+
guard let refactorCommand = refactorCommand as? ExpandMacroCommand else {
65+
throw ResponseError.unknown("refactorCommand is not a ExpandMacroCommand")
66+
}
67+
68+
let expansion = try await self.refactoring(refactorCommand, MacroExpansion.self)
69+
70+
for macroEdit in expansion.edits {
71+
let macroExpansionFilePath = self.generatedMacroExpansionsPath.appendingPathComponent(
72+
macroEdit.bufferName
73+
)
74+
let macroExpansionDocURI = DocumentURI(macroExpansionFilePath)
75+
if let _ = try? self.documentManager.latestSnapshot(macroExpansionDocURI) {
76+
continue
77+
}
78+
79+
do {
80+
try macroEdit.newText.write(to: macroExpansionFilePath, atomically: true, encoding: String.Encoding.utf8)
81+
} catch {
82+
throw ResponseError.unknown("Unable to write macro expansion to file path: \"\(macroExpansionFilePath.path)\"")
83+
}
84+
85+
let req = ShowDocumentRequest(uri: macroExpansionDocURI, selection: macroEdit.range)
86+
let response = try await sourceKitLSPServer.sendRequestToClient(req)
87+
if !response.success {
88+
logger.error("client refused to show document for \(expansion.title, privacy: .public)!")
89+
}
90+
}
91+
92+
return expansion.edits.encodeToLSPAny()
93+
}
94+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
/// Represents a macro expansion as an edit. Notionally, a subclass of `TextEdit`
17+
public struct MacroExpansionEdit: ResponseType, Hashable, Sendable {
18+
/// The range of text to be replaced.
19+
@CustomCodable<PositionRange>
20+
public var range: Range<Position>
21+
22+
/// The new text.
23+
public var newText: String
24+
25+
public var bufferName: String
26+
27+
public init(range: Range<Position>, newText: String, bufferName: String) {
28+
self._range = CustomCodable<PositionRange>(wrappedValue: range)
29+
self.newText = newText
30+
self.bufferName = bufferName
31+
}
32+
}
33+
34+
extension MacroExpansionEdit: LSPAnyCodable {
35+
public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
36+
guard case .dictionary(let rangeDict) = dictionary[CodingKeys.range.stringValue],
37+
case .string(let newText) = dictionary[CodingKeys.newText.stringValue],
38+
case .string(let bufferName) = dictionary[CodingKeys.bufferName.stringValue]
39+
else {
40+
return nil
41+
}
42+
guard let range = Range<Position>(fromLSPDictionary: rangeDict) else {
43+
return nil
44+
}
45+
self._range = CustomCodable<PositionRange>(wrappedValue: range)
46+
self.newText = newText
47+
self.bufferName = bufferName
48+
}
49+
50+
public func encodeToLSPAny() -> LSPAny {
51+
return .dictionary([
52+
CodingKeys.range.stringValue: range.encodeToLSPAny(),
53+
CodingKeys.newText.stringValue: .string(newText),
54+
CodingKeys.bufferName.stringValue: .string(bufferName),
55+
])
56+
}
57+
}

Sources/SourceKitLSP/Swift/SwiftCommand.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,20 @@ import LanguageServerProtocol
1515
///
1616
/// All commands from the Swift LSP should be listed here.
1717
public let builtinSwiftCommands: [String] = [
18-
SemanticRefactorCommand.self
19-
].map { $0.identifier }
18+
SemanticRefactorCommand.self,
19+
ExpandMacroCommand.self,
20+
].map { (command: any SwiftCommand.Type) in
21+
command.identifier
22+
}
2023

2124
/// A `Command` that should be executed by Swift's language server.
2225
public protocol SwiftCommand: Codable, Hashable, LSPAnyCodable {
2326
static var identifier: String { get }
2427
var title: String { get set }
2528
}
2629

30+
public typealias SwiftCommandHandler = (any SwiftCommand) async throws -> LSPAny
31+
2732
extension SwiftCommand {
2833
/// Converts this `SwiftCommand` to a generic LSP `Command` object.
2934
public func asCommand() throws -> Command {

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ public actor SwiftLanguageService: LanguageService, Sendable {
112112
/// Directory where generated Swift interfaces will be stored.
113113
let generatedInterfacesPath: URL
114114

115+
/// Directory where generated Macro expansions will be stored.
116+
let generatedMacroExpansionsPath: URL
117+
115118
// FIXME: ideally we wouldn't need separate management from a parent server in the same process.
116119
var documentManager: DocumentManager
117120

@@ -203,6 +206,8 @@ public actor SwiftLanguageService: LanguageService, Sendable {
203206
self.state = .connected
204207
self.generatedInterfacesPath = options.generatedInterfacesPath.asURL
205208
try FileManager.default.createDirectory(at: generatedInterfacesPath, withIntermediateDirectories: true)
209+
self.generatedMacroExpansionsPath = options.generatedMacroExpansionsPath.asURL
210+
try FileManager.default.createDirectory(at: generatedMacroExpansionsPath, withIntermediateDirectories: true)
206211
self.diagnosticReportManager = nil // Needed to work around rdar://116221716
207212

208213
// The debounce duration of 500ms was chosen arbitrarily without scientific research.
@@ -919,29 +924,19 @@ extension SwiftLanguageService {
919924
}
920925

921926
public func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? {
922-
// TODO: If there's support for several types of commands, we might need to structure this similarly to the code actions request.
923-
guard let sourceKitLSPServer else {
924-
// `SourceKitLSPServer` has been destructed. We are tearing down the language
925-
// server. Nothing left to do.
926-
throw ResponseError.unknown("Connection to the editor closed")
927-
}
928-
guard let swiftCommand = req.swiftCommand(ofType: SemanticRefactorCommand.self) else {
929-
throw ResponseError.unknown("semantic refactoring: unknown command \(req.command)")
930-
}
931-
let refactor = try await semanticRefactoring(swiftCommand)
932-
let edit = refactor.edit
933-
let req = ApplyEditRequest(label: refactor.title, edit: edit)
934-
let response = try await sourceKitLSPServer.sendRequestToClient(req)
935-
if !response.applied {
936-
let reason: String
937-
if let failureReason = response.failureReason {
938-
reason = " reason: \(failureReason)"
939-
} else {
940-
reason = ""
927+
928+
let commandsAndHandlers: [(command: any SwiftCommand.Type, handler: SwiftCommandHandler)] = [
929+
(command: SemanticRefactorCommand.self, handler: semanticRefactoring),
930+
(command: ExpandMacroCommand.self, handler: expandMacro),
931+
]
932+
933+
for (command, handler) in commandsAndHandlers {
934+
if let swiftCommand = req.swiftCommand(ofType: command) {
935+
return try await handler(swiftCommand)
941936
}
942-
logger.error("client refused to apply edit for \(refactor.title, privacy: .public)!\(reason)")
943937
}
944-
return edit.encodeToLSPAny()
938+
939+
throw ResponseError.unknown("semantic refactoring: unknown command \(req.command)")
945940
}
946941
}
947942

Sources/sourcekit-lsp/SourceKitLSP.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ struct SourceKitLSP: AsyncParsableCommand {
193193
)
194194
var generatedInterfacesPath = defaultDirectoryForGeneratedInterfaces
195195

196+
@Option(
197+
help: "Specify the directory where generated macro expansions will be stored"
198+
)
199+
var generatedMacroExpansionsPath = defaultDirectoryForGeneratedMacroExpansions
200+
196201
@Option(
197202
help: "When server-side filtering is enabled, the maximum number of results to return"
198203
)
@@ -224,6 +229,7 @@ struct SourceKitLSP: AsyncParsableCommand {
224229
serverOptions.indexOptions.indexPrefixMappings = indexPrefixMappings
225230
serverOptions.completionOptions.maxResults = completionMaxResults
226231
serverOptions.generatedInterfacesPath = generatedInterfacesPath
232+
serverOptions.generatedMacroExpansionsPath = generatedMacroExpansionsPath
227233
serverOptions.experimentalFeatures = Set(experimentalFeatures)
228234

229235
return serverOptions

0 commit comments

Comments
 (0)