Skip to content

Commit c31dff2

Browse files
authored
Merge pull request #668 from bwhiteley/textualinterface
SourceKitLSP: generate Swift textual interfaces for module references
2 parents 5b16c3e + d2f7f2f commit c31dff2

17 files changed

+361
-13
lines changed

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public let builtinRequests: [_RequestType.Type] = [
5656
InlineValueRequest.self,
5757
LinkedEditingRangeRequest.self,
5858
MonikersRequest.self,
59+
OpenInterfaceRequest.self,
5960
PollIndexRequest.self,
6061
PrepareRenameRequest.self,
6162
ReferencesRequest.self,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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 a textual interface of a module to display in the IDE.
14+
/// **(LSP Extension)**
15+
public struct OpenInterfaceRequest: TextDocumentRequest, Hashable {
16+
public static let method: String = "textDocument/openInterface"
17+
public typealias Response = InterfaceDetails?
18+
19+
/// The document whose compiler arguments should be used to generate the interface.
20+
public var textDocument: TextDocumentIdentifier
21+
22+
/// The module to generate an index for.
23+
public var name: String
24+
25+
public init(textDocument: TextDocumentIdentifier, name: String) {
26+
self.textDocument = textDocument
27+
self.name = name
28+
}
29+
}
30+
31+
/// The textual output of a module interface.
32+
public struct InterfaceDetails: ResponseType, Hashable {
33+
34+
public var uri: DocumentURI
35+
36+
public init(uri: DocumentURI) {
37+
self.uri = uri
38+
}
39+
}

Sources/LanguageServerProtocol/Requests/SymbolInfoRequest.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,20 @@ public struct SymbolDetails: ResponseType, Hashable {
7878
/// translation unit.
7979
public var bestLocalDeclaration: Location? = nil
8080

81+
/// The kind of the symbol
82+
public var kind: SymbolKind?
83+
8184
public init(
8285
name: String?,
8386
containerName: String? = nil,
8487
usr: String?,
85-
bestLocalDeclaration: Location? = nil)
88+
bestLocalDeclaration: Location? = nil,
89+
kind: SymbolKind? = nil)
8690
{
8791
self.name = name
8892
self.containerName = containerName
8993
self.usr = usr
9094
self.bestLocalDeclaration = bestLocalDeclaration
95+
self.kind = kind
9196
}
9297
}

Sources/SKSupport/FileSystem.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@ extension AbsolutePath {
3030
}
3131
}
3232
}
33+
34+
/// The directory to write generated module interfaces
35+
public var defaultDirectoryForGeneratedInterfaces: AbsolutePath {
36+
return AbsolutePath(NSTemporaryDirectory()).appending(component: "GeneratedInterfaces")
37+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import lib
1+
import /*lib:import*/lib
22

33
Lib() . /*Lib.foo:call*/foo()

Sources/SourceKitD/SourceKitD.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ extension SourceKitD {
112112

113113
return handle
114114
}
115+
116+
public func cancel(_ handle: sourcekitd_request_handle_t) {
117+
api.cancel_request(handle)
118+
}
115119
}
116120

117121
private func logRequest(_ request: SKDRequestDictionary) {

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public struct sourcekitd_keys {
4545
public let kind: sourcekitd_uid_t
4646
public let length: sourcekitd_uid_t
4747
public let line: sourcekitd_uid_t
48+
public let modulename: sourcekitd_uid_t
4849
public let name: sourcekitd_uid_t
4950
public let namelength: sourcekitd_uid_t
5051
public let nameoffset: sourcekitd_uid_t
@@ -63,6 +64,7 @@ public struct sourcekitd_keys {
6364
public let substructure: sourcekitd_uid_t
6465
public let syntactic_only: sourcekitd_uid_t
6566
public let syntaxmap: sourcekitd_uid_t
67+
public let synthesizedextensions: sourcekitd_uid_t
6668
public let enablesyntaxmap: sourcekitd_uid_t
6769
public let text: sourcekitd_uid_t
6870
public let typename: sourcekitd_uid_t
@@ -118,6 +120,7 @@ public struct sourcekitd_keys {
118120
kind = api.uid_get_from_cstr("key.kind")!
119121
length = api.uid_get_from_cstr("key.length")!
120122
line = api.uid_get_from_cstr("key.line")!
123+
modulename = api.uid_get_from_cstr("key.modulename")!
121124
name = api.uid_get_from_cstr("key.name")!
122125
namelength = api.uid_get_from_cstr("key.namelength")!
123126
nameoffset = api.uid_get_from_cstr("key.nameoffset")!
@@ -137,6 +140,7 @@ public struct sourcekitd_keys {
137140
syntactic_only = api.uid_get_from_cstr("key.syntactic_only")!
138141
syntaxmap = api.uid_get_from_cstr("key.syntaxmap")!
139142
enablesyntaxmap = api.uid_get_from_cstr("key.enablesyntaxmap")!
143+
synthesizedextensions = api.uid_get_from_cstr("key.synthesizedextensions")!
140144
text = api.uid_get_from_cstr("key.text")!
141145
typename = api.uid_get_from_cstr("key.typename")!
142146
usr = api.uid_get_from_cstr("key.usr")!
@@ -163,6 +167,7 @@ public struct sourcekitd_keys {
163167
public struct sourcekitd_requests {
164168
public let crash_exit: sourcekitd_uid_t
165169
public let editor_open: sourcekitd_uid_t
170+
public let editor_open_interface: sourcekitd_uid_t
166171
public let editor_close: sourcekitd_uid_t
167172
public let editor_replacetext: sourcekitd_uid_t
168173
public let codecomplete: sourcekitd_uid_t
@@ -178,6 +183,7 @@ public struct sourcekitd_requests {
178183
public init(api: sourcekitd_functions_t) {
179184
crash_exit = api.uid_get_from_cstr("source.request.crash_exit")!
180185
editor_open = api.uid_get_from_cstr("source.request.editor.open")!
186+
editor_open_interface = api.uid_get_from_cstr("source.request.editor.open.interface")!
181187
editor_close = api.uid_get_from_cstr("source.request.editor.close")!
182188
editor_replacetext = api.uid_get_from_cstr("source.request.editor.replacetext")!
183189
codecomplete = api.uid_get_from_cstr("source.request.codecomplete")!

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@ extension ClangLanguageServerShim {
522522
}
523523
}
524524

525+
func openInterface(_ request: Request<OpenInterfaceRequest>) {
526+
request.reply(.failure(.unknown("unsupported method")))
527+
}
528+
525529
// MARK: - Other
526530

527531
func executeCommand(_ req: Request<ExecuteCommandRequest>) {

Sources/SourceKitLSP/SourceKitServer+Options.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import LanguageServerProtocol
1414
import SKCore
15+
import struct TSCBasic.AbsolutePath
16+
import SKSupport
1517

1618
extension SourceKitServer {
1719

@@ -30,17 +32,22 @@ extension SourceKitServer {
3032

3133
/// Options for code-completion.
3234
public var completionOptions: SKCompletionOptions
35+
36+
/// Override the default directory where generated interfaces will be stored
37+
public var generatedInterfacesPath: AbsolutePath
3338

3439
public init(
3540
buildSetup: BuildSetup = .default,
3641
clangdOptions: [String] = [],
3742
indexOptions: IndexOptions = .init(),
38-
completionOptions: SKCompletionOptions = .init())
43+
completionOptions: SKCompletionOptions = .init(),
44+
generatedInterfacesPath: AbsolutePath = defaultDirectoryForGeneratedInterfaces)
3945
{
4046
self.buildSetup = buildSetup
4147
self.clangdOptions = clangdOptions
4248
self.indexOptions = indexOptions
4349
self.completionOptions = completionOptions
50+
self.generatedInterfacesPath = generatedInterfacesPath
4451
}
4552
}
4653
}

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ public final class SourceKitServer: LanguageServer {
184184
registerToolchainTextDocumentRequest(SourceKitServer.completion,
185185
CompletionList(isIncomplete: false, items: []))
186186
registerToolchainTextDocumentRequest(SourceKitServer.hover, nil)
187+
registerToolchainTextDocumentRequest(SourceKitServer.openInterface, nil)
187188
registerToolchainTextDocumentRequest(SourceKitServer.declaration, .locations([]))
188189
registerToolchainTextDocumentRequest(SourceKitServer.definition, .locations([]))
189190
registerToolchainTextDocumentRequest(SourceKitServer.references, [])
@@ -985,6 +986,14 @@ extension SourceKitServer {
985986
) {
986987
languageService.hover(req)
987988
}
989+
990+
func openInterface(
991+
_ req: Request<OpenInterfaceRequest>,
992+
workspace: Workspace,
993+
languageService: ToolchainLanguageServer
994+
) {
995+
languageService.openInterface(req)
996+
}
988997

989998
/// Find all symbols in the workspace that include a string in their name.
990999
/// - returns: An array of SymbolOccurrences that match the string.
@@ -1272,6 +1281,26 @@ extension SourceKitServer {
12721281
let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position)
12731282
let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index
12741283
let callback = callbackOnQueue(self.queue) { (result: LSPResult<SymbolInfoRequest.Response>) in
1284+
1285+
// If this symbol is a module then generate a textual interface
1286+
if case .success(let symbols) = result, let symbol = symbols.first, symbol.kind == .module, let name = symbol.name {
1287+
let openInterface = OpenInterfaceRequest(textDocument: req.params.textDocument, name: name)
1288+
let request = Request(openInterface, id: req.id, clientID: ObjectIdentifier(self),
1289+
cancellation: req.cancellationToken, reply: { (result: Result<OpenInterfaceRequest.Response, ResponseError>) in
1290+
switch result {
1291+
case .success(let interfaceDetails?):
1292+
let loc = Location(uri: interfaceDetails.uri, range: Range(Position(line: 0, utf16index: 0)))
1293+
req.reply(.locations([loc]))
1294+
case .success(nil):
1295+
req.reply(.failure(.unknown("Could not generate Swift Interface for \(name)")))
1296+
case .failure(let error):
1297+
req.reply(.failure(error))
1298+
}
1299+
})
1300+
languageService.openInterface(request)
1301+
return
1302+
}
1303+
12751304
let extractedResult = self.extractIndexedOccurrences(result: result, index: index, useLocalFallback: true) { (usr, index) in
12761305
log("performing indexed jump-to-def with usr \(usr)")
12771306
var occurs = index.occurrences(ofUSR: usr, roles: [.definition])

Sources/SourceKitLSP/Swift/CursorInfo.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ extension SwiftLanguageServer {
109109
return completion(.failure(.responseError(ResponseError(result.failure!))))
110110
}
111111

112-
guard let _: sourcekitd_uid_t = dict[keys.kind] else {
112+
guard let kind: sourcekitd_uid_t = dict[keys.kind] else {
113113
// Nothing to report.
114114
return completion(.success(nil))
115115
}
@@ -130,7 +130,8 @@ extension SwiftLanguageServer {
130130
name: dict[keys.name],
131131
containerName: nil,
132132
usr: dict[keys.usr],
133-
bestLocalDeclaration: location),
133+
bestLocalDeclaration: location,
134+
kind: kind.asSymbolKind(self.sourcekitd.values)),
134135
annotatedDeclaration: dict[keys.annotated_decl],
135136
documentationXML: dict[keys.doc_full_as_xml],
136137
refactorActions:
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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 SourceKitD
15+
import LanguageServerProtocol
16+
import LSPLogging
17+
18+
struct InterfaceInfo {
19+
var contents: String
20+
}
21+
22+
extension SwiftLanguageServer {
23+
public func openInterface(_ request: LanguageServerProtocol.Request<LanguageServerProtocol.OpenInterfaceRequest>) {
24+
let uri = request.params.textDocument.uri
25+
let moduleName = request.params.name
26+
self.queue.async {
27+
let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(moduleName).swiftinterface")
28+
let interfaceDocURI = DocumentURI(interfaceFilePath)
29+
self._openInterface(request: request, uri: uri, name: moduleName, interfaceURI: interfaceDocURI) { result in
30+
switch result {
31+
case .success(let interfaceInfo):
32+
do {
33+
try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8)
34+
request.reply(.success(InterfaceDetails(uri: interfaceDocURI)))
35+
} catch {
36+
request.reply(.failure(ResponseError.unknown(error.localizedDescription)))
37+
}
38+
case .failure(let error):
39+
log("open interface failed: \(error)", level: .warning)
40+
request.reply(.failure(ResponseError(error)))
41+
}
42+
}
43+
}
44+
}
45+
46+
/// Open the Swift interface for a module.
47+
///
48+
/// - Parameters:
49+
/// - request: The OpenInterfaceRequest.
50+
/// - uri: The document whose compiler arguments should be used to generate the interface.
51+
/// - name: The name of the module whose interface should be generated.
52+
/// - interfaceURI: The file where the generated interface should be written.
53+
/// - completion: Completion block to asynchronously receive the InterfaceInfo, or error.
54+
private func _openInterface(request: LanguageServerProtocol.Request<LanguageServerProtocol.OpenInterfaceRequest>,
55+
uri: DocumentURI,
56+
name: String,
57+
interfaceURI: DocumentURI,
58+
completion: @escaping (Swift.Result<InterfaceInfo, SKDError>) -> Void) {
59+
let keys = self.keys
60+
let skreq = SKDRequestDictionary(sourcekitd: sourcekitd)
61+
skreq[keys.request] = requests.editor_open_interface
62+
skreq[keys.modulename] = name
63+
skreq[keys.name] = interfaceURI.pseudoPath
64+
skreq[keys.synthesizedextensions] = 1
65+
if let compileCommand = self.commandsByFile[uri] {
66+
skreq[keys.compilerargs] = compileCommand.compilerArgs
67+
}
68+
69+
let handle = self.sourcekitd.send(skreq, self.queue) { result in
70+
switch result {
71+
case .success(let dict):
72+
return completion(.success(InterfaceInfo(contents: dict[keys.sourcetext] ?? "")))
73+
case .failure(let error):
74+
return completion(.failure(error))
75+
}
76+
}
77+
78+
if let handle = handle {
79+
request.cancellationToken.addCancellationHandler { [weak self] in
80+
self?.sourcekitd.cancel(handle)
81+
}
82+
}
83+
}
84+
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

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

13+
import Foundation
1314
import Dispatch
14-
import struct Foundation.CharacterSet
1515
import LanguageServerProtocol
1616
import LSPLogging
1717
import SKCore
@@ -106,6 +106,9 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
106106
let clientCapabilities: ClientCapabilities
107107

108108
let serverOptions: SourceKitServer.Options
109+
110+
/// Directory where generated Swift interfaces will be stored.
111+
let generatedInterfacesPath: URL
109112

110113
// FIXME: ideally we wouldn't need separate management from a parent server in the same process.
111114
var documentManager: DocumentManager
@@ -154,6 +157,8 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
154157
self.documentManager = DocumentManager()
155158
self.state = .connected
156159
self.reopenDocuments = reopenDocuments
160+
self.generatedInterfacesPath = options.generatedInterfacesPath.asURL
161+
try FileManager.default.createDirectory(at: generatedInterfacesPath, withIntermediateDirectories: true)
157162
}
158163

159164
public func canHandle(workspace: Workspace) -> Bool {
@@ -259,7 +264,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
259264
}
260265
})
261266
}
262-
267+
263268
/// Publish diagnostics for the given `snapshot`. We withhold semantic diagnostics if we are using
264269
/// fallback arguments.
265270
///
@@ -1576,6 +1581,8 @@ extension sourcekitd_uid_t {
15761581
case vals.decl_extension:
15771582
// There are no extensions in LSP, so I return something vaguely similar
15781583
return .namespace
1584+
case vals.ref_module:
1585+
return .module
15791586
default:
15801587
return nil
15811588
}

Sources/SourceKitLSP/ToolchainLanguageServer.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public protocol ToolchainLanguageServer: AnyObject {
8181
func completion(_ req: Request<CompletionRequest>)
8282
func hover(_ req: Request<HoverRequest>)
8383
func symbolInfo(_ request: Request<SymbolInfoRequest>)
84+
func openInterface(_ request: Request<OpenInterfaceRequest>)
8485

8586
/// Returns true if the `ToolchainLanguageServer` will take ownership of the request.
8687
func definition(_ request: Request<DefinitionRequest>) -> Bool

0 commit comments

Comments
 (0)