Skip to content

Commit 9e6c7e0

Browse files
authored
Merge pull request #1436 from lokesh-tr/gsoc24-expansion-of-macros-in-vscode
Add LSP support for showing `@freestanding` Macro Expansions
2 parents a111b47 + fd50560 commit 9e6c7e0

19 files changed

+754
-178
lines changed

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ add_library(LanguageServerProtocol STATIC
7373
Requests/RegisterCapabilityRequest.swift
7474
Requests/RenameRequest.swift
7575
Requests/SelectionRangeRequest.swift
76+
Requests/ShowDocumentRequest.swift
7677
Requests/ShowMessageRequest.swift
7778
Requests/ShutdownRequest.swift
7879
Requests/SignatureHelpRequest.swift

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public let builtinRequests: [_RequestType.Type] = [
6565
RegisterCapabilityRequest.self,
6666
RenameRequest.self,
6767
SelectionRangeRequest.self,
68+
ShowDocumentRequest.self,
6869
ShowMessageRequest.self,
6970
ShutdownRequest.self,
7071
SignatureHelpRequest.self,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 server to the client to show a document on the client
14+
/// side.
15+
public struct ShowDocumentRequest: RequestType {
16+
public static let method: String = "window/showDocument"
17+
public typealias Response = ShowDocumentResponse
18+
19+
/// The uri to show.
20+
public var uri: DocumentURI
21+
22+
/// An optional boolean indicates to show the resource in an external
23+
/// program. To show, for example, `https://www.swift.org/ in the default WEB
24+
/// browser set `external` to `true`.
25+
public var external: Bool?
26+
27+
/// An optional boolean to indicate whether the editor showing the document
28+
/// should take focus or not. Clients might ignore this property if an
29+
/// external program is started.
30+
public var takeFocus: Bool?
31+
32+
/// An optional selection range if the document is a text document. Clients
33+
/// might ignore the property if an external program is started or the file
34+
/// is not a text file.
35+
public var selection: Range<Position>?
36+
37+
public init(uri: DocumentURI, external: Bool? = nil, takeFocus: Bool? = nil, selection: Range<Position>? = nil) {
38+
self.uri = uri
39+
self.external = external
40+
self.takeFocus = takeFocus
41+
self.selection = selection
42+
}
43+
}
44+
45+
public struct ShowDocumentResponse: Codable, Hashable, ResponseType {
46+
/// A boolean indicating if the show was successful.
47+
public var success: Bool
48+
49+
public init(success: Bool) {
50+
self.success = success
51+
}
52+
}

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)