Skip to content

Commit 607292a

Browse files
authored
Merge pull request #1479 from lokesh-tr/show-attached-macro-expansions
Add LSP extension to show Macro Expansions (or any document) in a "peeked" editor (and some minor quality improvements)
2 parents 59711df + 0221475 commit 607292a

14 files changed

+556
-161
lines changed

Documentation/LSP Extensions.md

+37
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,40 @@ Users should not need to rely on this request. The index should always be update
436436
```ts
437437
export interface TriggerReindexParams {}
438438
```
439+
440+
## `workspace/peekDocuments`
441+
442+
Request from the server to the client to show the given documents in a "peeked" editor.
443+
444+
This request is handled by the client to show the given documents in a "peeked" editor (i.e. inline with / inside the editor canvas).
445+
446+
It requires the experimental client capability `"workspace/peekDocuments"` to use.
447+
448+
- params: `PeekDocumentsParams`
449+
- result: `PeekDocumentsResult`
450+
451+
```ts
452+
export interface PeekDocumentsParams {
453+
/**
454+
* The `DocumentUri` of the text document in which to show the "peeked" editor
455+
*/
456+
uri: DocumentUri;
457+
458+
/**
459+
* The `Position` in the given text document in which to show the "peeked editor"
460+
*/
461+
position: Position;
462+
463+
/**
464+
* An array `DocumentUri` of the documents to appear inside the "peeked" editor
465+
*/
466+
locations: DocumentUri[];
467+
}
468+
469+
/**
470+
* Response to indicate the `success` of the `PeekDocumentsRequest`
471+
*/
472+
export interface PeekDocumentsResult {
473+
success: boolean;
474+
}
475+
```

Package.swift

+1
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ let package = Package(
380380
"SwiftExtensions",
381381
.product(name: "IndexStoreDB", package: "indexstore-db"),
382382
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
383+
.product(name: "Crypto", package: "swift-crypto"),
383384
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
384385
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
385386
.product(name: "SwiftParser", package: "swift-syntax"),

Sources/LanguageServerProtocol/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ add_library(LanguageServerProtocol STATIC
6666
Requests/InlineValueRequest.swift
6767
Requests/LinkedEditingRangeRequest.swift
6868
Requests/MonikersRequest.swift
69+
Requests/PeekDocumentsRequest.swift
6970
Requests/PollIndexRequest.swift
7071
Requests/PrepareRenameRequest.swift
7172
Requests/ReferencesRequest.swift

Sources/LanguageServerProtocol/Messages.swift

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public let builtinRequests: [_RequestType.Type] = [
5858
InlineValueRequest.self,
5959
LinkedEditingRangeRequest.self,
6060
MonikersRequest.self,
61+
PeekDocumentsRequest.self,
6162
PollIndexRequest.self,
6263
PrepareRenameRequest.self,
6364
ReferencesRequest.self,
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+
/// Request from the server to the client to show the given documents in a "peeked" editor **(LSP Extension)**
14+
///
15+
/// This request is handled by the client to show the given documents in a
16+
/// "peeked" editor (i.e. inline with / inside the editor canvas). This is
17+
/// similar to VS Code's built-in "editor.action.peekLocations" command.
18+
///
19+
/// - Parameters:
20+
/// - uri: The DocumentURI of the text document in which to show the "peeked" editor
21+
/// - position: The position in the given text document in which to show the "peeked editor"
22+
/// - locations: The DocumentURIs of documents to appear inside the "peeked" editor
23+
///
24+
/// - Returns: `PeekDocumentsResponse` which indicates the `success` of the request.
25+
///
26+
/// ### LSP Extension
27+
///
28+
/// This request is an extension to LSP supported by SourceKit-LSP.
29+
/// It requires the experimental client capability `"workspace/peekDocuments"` to use.
30+
/// It also needs the client to handle the request and present the "peeked" editor.
31+
public struct PeekDocumentsRequest: RequestType {
32+
public static let method: String = "workspace/peekDocuments"
33+
public typealias Response = PeekDocumentsResponse
34+
35+
public var uri: DocumentURI
36+
public var position: Position
37+
public var locations: [DocumentURI]
38+
39+
public init(
40+
uri: DocumentURI,
41+
position: Position,
42+
locations: [DocumentURI]
43+
) {
44+
self.uri = uri
45+
self.position = position
46+
self.locations = locations
47+
}
48+
}
49+
50+
/// Response to indicate the `success` of the `PeekDocumentsRequest`
51+
public struct PeekDocumentsResponse: ResponseType {
52+
public var success: Bool
53+
54+
public init(success: Bool) {
55+
self.success = success
56+
}
57+
}

Sources/LanguageServerProtocol/SupportTypes/WorkspaceEdit.swift

-32
Original file line numberDiff line numberDiff line change
@@ -303,35 +303,3 @@ public struct DeleteFile: Codable, Hashable, Sendable {
303303
try container.encodeIfPresent(self.annotationId, forKey: .annotationId)
304304
}
305305
}
306-
307-
extension WorkspaceEdit: LSPAnyCodable {
308-
public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
309-
guard case .dictionary(let lspDict) = dictionary[CodingKeys.changes.stringValue] else {
310-
return nil
311-
}
312-
var dictionary = [DocumentURI: [TextEdit]]()
313-
for (key, value) in lspDict {
314-
guard
315-
let uri = try? DocumentURI(string: key),
316-
let edits = [TextEdit](fromLSPArray: value)
317-
else {
318-
return nil
319-
}
320-
dictionary[uri] = edits
321-
}
322-
self.changes = dictionary
323-
}
324-
325-
public func encodeToLSPAny() -> LSPAny {
326-
guard let changes = changes else {
327-
return nil
328-
}
329-
let values = changes.map {
330-
($0.key.stringValue, $0.value.encodeToLSPAny())
331-
}
332-
let dictionary = Dictionary(uniqueKeysWithValues: values)
333-
return .dictionary([
334-
CodingKeys.changes.stringValue: .dictionary(dictionary)
335-
])
336-
}
337-
}

Sources/SourceKitLSP/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ target_sources(SourceKitLSP PRIVATE
4747
Swift/FoldingRange.swift
4848
Swift/MacroExpansion.swift
4949
Swift/OpenInterface.swift
50-
Swift/Refactoring.swift
50+
Swift/RefactoringResponse.swift
5151
Swift/RefactoringEdit.swift
5252
Swift/RefactorCommand.swift
5353
Swift/RelatedIdentifiers.swift

Sources/SourceKitLSP/SourceKitLSPServer.swift

+19-1
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,25 @@ extension SourceKitLSPServer {
956956
}
957957

958958
func initialize(_ req: InitializeRequest) async throws -> InitializeResult {
959-
capabilityRegistry = CapabilityRegistry(clientCapabilities: req.capabilities)
959+
// If the client can handle `PeekDocumentsRequest`, they can enable the
960+
// experimental client capability `"workspace/peekDocuments"` through the `req.capabilities.experimental`.
961+
//
962+
// The below is a workaround for the vscode-swift extension since it cannot set client capabilities.
963+
// It passes "workspace/peekDocuments" through the `initializationOptions`.
964+
var clientCapabilities = req.capabilities
965+
if case .dictionary(let initializationOptions) = req.initializationOptions,
966+
let peekDocuments = initializationOptions["workspace/peekDocuments"]
967+
{
968+
if case .dictionary(var experimentalCapabilities) = clientCapabilities.experimental {
969+
experimentalCapabilities["workspace/peekDocuments"] = peekDocuments
970+
clientCapabilities.experimental = .dictionary(experimentalCapabilities)
971+
} else {
972+
clientCapabilities.experimental = .dictionary(["workspace/peekDocuments": peekDocuments])
973+
}
974+
}
975+
976+
capabilityRegistry = CapabilityRegistry(clientCapabilities: clientCapabilities)
977+
960978
self.options = SourceKitLSPOptions.merging(
961979
base: self.options,
962980
override: orLog("Parsing SourceKitLSPOptions", { try SourceKitLSPOptions(fromLSPAny: req.initializationOptions) })

Sources/SourceKitLSP/Swift/MacroExpansion.swift

+96-17
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import Crypto
1314
import Foundation
1415
import LSPLogging
1516
import LanguageServerProtocol
@@ -46,17 +47,15 @@ struct MacroExpansion: RefactoringResponse {
4647
extension SwiftLanguageService {
4748
/// Handles the `ExpandMacroCommand`.
4849
///
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.
50+
/// Makes a `PeekDocumentsRequest` or `ShowDocumentRequest`, containing the
51+
/// location of each macro expansion, to the client depending on whether the
52+
/// client supports the `experimental["workspace/peekDocuments"]` capability.
5253
///
5354
/// - Parameters:
5455
/// - expandMacroCommand: The `ExpandMacroCommand` that triggered this request.
55-
///
56-
/// - Returns: A `[RefactoringEdit]` with the necessary edits and buffer name as a `LSPAny`
5756
func expandMacro(
5857
_ expandMacroCommand: ExpandMacroCommand
59-
) async throws -> LSPAny {
58+
) async throws {
6059
guard let sourceKitLSPServer else {
6160
// `SourceKitLSPServer` has been destructed. We are tearing down the
6261
// language server. Nothing left to do.
@@ -69,6 +68,10 @@ extension SwiftLanguageService {
6968

7069
let expansion = try await self.refactoring(expandMacroCommand)
7170

71+
var completeExpansionFileContent = ""
72+
var completeExpansionDirectoryName = ""
73+
74+
var macroExpansionFilePaths: [URL] = []
7275
for macroEdit in expansion.edits {
7376
if let bufferName = macroEdit.bufferName {
7477
// buffer name without ".swift"
@@ -79,6 +82,9 @@ extension SwiftLanguageService {
7982

8083
let macroExpansionBufferDirectoryURL = self.generatedMacroExpansionsPath
8184
.appendingPathComponent(macroExpansionBufferDirectoryName)
85+
86+
completeExpansionDirectoryName += "\(bufferName)-"
87+
8288
do {
8389
try FileManager.default.createDirectory(
8490
at: macroExpansionBufferDirectoryURL,
@@ -95,7 +101,7 @@ extension SwiftLanguageService {
95101

96102
// github permalink notation for position range
97103
let macroExpansionPositionRangeIndicator =
98-
"L\(macroEdit.range.lowerBound.line)C\(macroEdit.range.lowerBound.utf16index)-L\(macroEdit.range.upperBound.line)C\(macroEdit.range.upperBound.utf16index)"
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)"
99105

100106
let macroExpansionFilePath =
101107
macroExpansionBufferDirectoryURL
@@ -111,22 +117,95 @@ extension SwiftLanguageService {
111117
)
112118
}
113119

114-
Task {
115-
let req = ShowDocumentRequest(uri: DocumentURI(macroExpansionFilePath), selection: macroEdit.range)
120+
macroExpansionFilePaths.append(macroExpansionFilePath)
116121

117-
let response = await orLog("Sending ShowDocumentRequest to Client") {
118-
try await sourceKitLSPServer.sendRequestToClient(req)
119-
}
122+
let editContent =
123+
"""
124+
// \(sourceFileURL.lastPathComponent) @ \(macroEdit.range.lowerBound.line + 1):\(macroEdit.range.lowerBound.utf16index + 1) - \(macroEdit.range.upperBound.line + 1):\(macroEdit.range.upperBound.utf16index + 1)
125+
\(macroEdit.newText)
120126
121-
if let response, !response.success {
122-
logger.error("client refused to show document for \(expansion.title, privacy: .public)")
123-
}
124-
}
127+
128+
"""
129+
completeExpansionFileContent += editContent
125130
} else if !macroEdit.newText.isEmpty {
126131
logger.fault("Unable to retrieve some parts of macro expansion")
127132
}
128133
}
129134

130-
return expansion.edits.encodeToLSPAny()
135+
// removes superfluous newline
136+
if completeExpansionFileContent.hasSuffix("\n\n") {
137+
completeExpansionFileContent.removeLast()
138+
}
139+
140+
if completeExpansionDirectoryName.hasSuffix("-") {
141+
completeExpansionDirectoryName.removeLast()
142+
}
143+
144+
var completeExpansionFilePath =
145+
self.generatedMacroExpansionsPath.appendingPathComponent(
146+
Insecure.MD5.hash(
147+
data: Data(completeExpansionDirectoryName.utf8)
148+
)
149+
.map { String(format: "%02hhx", $0) } // maps each byte of the hash to its hex equivalent `String`
150+
.joined()
151+
)
152+
153+
do {
154+
try FileManager.default.createDirectory(
155+
at: completeExpansionFilePath,
156+
withIntermediateDirectories: true
157+
)
158+
} catch {
159+
throw ResponseError.unknown(
160+
"Failed to create directory for complete macro expansion at path: \(completeExpansionFilePath.path)"
161+
)
162+
}
163+
164+
completeExpansionFilePath =
165+
completeExpansionFilePath.appendingPathComponent(sourceFileURL.lastPathComponent)
166+
do {
167+
try completeExpansionFileContent.write(to: completeExpansionFilePath, atomically: true, encoding: .utf8)
168+
} catch {
169+
throw ResponseError.unknown(
170+
"Unable to write complete macro expansion to file path: \"\(completeExpansionFilePath.path)\""
171+
)
172+
}
173+
174+
let completeMacroExpansionFilePath = completeExpansionFilePath
175+
let expansionURIs = macroExpansionFilePaths.map {
176+
return DocumentURI($0)
177+
}
178+
179+
if case .dictionary(let experimentalCapabilities) = self.capabilityRegistry.clientCapabilities.experimental,
180+
case .bool(true) = experimentalCapabilities["workspace/peekDocuments"]
181+
{
182+
Task {
183+
let req = PeekDocumentsRequest(
184+
uri: expandMacroCommand.textDocument.uri,
185+
position: expandMacroCommand.positionRange.lowerBound,
186+
locations: expansionURIs
187+
)
188+
189+
let response = await orLog("Sending PeekDocumentsRequest to Client") {
190+
try await sourceKitLSPServer.sendRequestToClient(req)
191+
}
192+
193+
if let response, !response.success {
194+
logger.error("client refused to peek macro")
195+
}
196+
}
197+
} else {
198+
Task {
199+
let req = ShowDocumentRequest(uri: DocumentURI(completeMacroExpansionFilePath))
200+
201+
let response = await orLog("Sending ShowDocumentRequest to Client") {
202+
try await sourceKitLSPServer.sendRequestToClient(req)
203+
}
204+
205+
if let response, !response.success {
206+
logger.error("client refused to show document for macro expansion")
207+
}
208+
}
209+
}
131210
}
132211
}

Sources/SourceKitLSP/Swift/Refactoring.swift renamed to Sources/SourceKitLSP/Swift/RefactoringResponse.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ extension RefactoringResponse {
3333
return nil
3434
}
3535

36-
var refactoringEdits = [RefactoringEdit]()
36+
var refactoringEdits: [RefactoringEdit] = []
3737

3838
categorizedEdits.forEach { _, categorizedEdit in
3939
guard let edits: SKDResponseArray = categorizedEdit[keys.edits] else {

Sources/SourceKitLSP/Swift/SemanticRefactorCommand.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ extension Array where Element == SemanticRefactorCommand {
8080
guard let results = array else {
8181
return nil
8282
}
83-
var commands = [SemanticRefactorCommand]()
83+
var commands: [SemanticRefactorCommand] = []
8484
results.forEach { _, value in
8585
if let name: String = value[keys.actionName],
8686
let actionuid: sourcekitd_api_uid_t = value[keys.actionUID],

Sources/SourceKitLSP/Swift/SemanticRefactoring.swift

+1-5
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,9 @@ extension SwiftLanguageService {
6969
///
7070
/// - Parameters:
7171
/// - semanticRefactorCommand: The `SemanticRefactorCommand` that triggered this request.
72-
///
73-
/// - Returns: A `WorkspaceEdit` with the necessary refactors as a `LSPAny`
7472
func semanticRefactoring(
7573
_ semanticRefactorCommand: SemanticRefactorCommand
76-
) async throws -> LSPAny {
74+
) async throws {
7775
guard let sourceKitLSPServer else {
7876
// `SourceKitLSPServer` has been destructed. We are tearing down the
7977
// language server. Nothing left to do.
@@ -94,7 +92,5 @@ extension SwiftLanguageService {
9492
}
9593
logger.error("client refused to apply edit for \(semanticRefactor.title, privacy: .public) \(reason)")
9694
}
97-
98-
return edit.encodeToLSPAny()
9995
}
10096
}

0 commit comments

Comments
 (0)