Skip to content

SourceKitLSP: generate Swift textual interfaces for module references #668

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public let builtinRequests: [_RequestType.Type] = [
InlineValueRequest.self,
LinkedEditingRangeRequest.self,
MonikersRequest.self,
OpenInterfaceRequest.self,
PollIndexRequest.self,
PrepareRenameRequest.self,
ReferencesRequest.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

/// Request a textual interface of a module to display in the IDE.
/// **(LSP Extension)**
public struct OpenInterfaceRequest: TextDocumentRequest, Hashable {
public static let method: String = "textDocument/openInterface"
public typealias Response = InterfaceDetails?

/// The document whose compiler arguments should be used to generate the interface.
public var textDocument: TextDocumentIdentifier

/// The module to generate an index for.
public var name: String

public init(textDocument: TextDocumentIdentifier, name: String) {
self.textDocument = textDocument
self.name = name
}
}

/// The textual output of a module interface.
public struct InterfaceDetails: ResponseType, Hashable {

public var uri: DocumentURI

public init(uri: DocumentURI) {
self.uri = uri
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,20 @@ public struct SymbolDetails: ResponseType, Hashable {
/// translation unit.
public var bestLocalDeclaration: Location? = nil

/// The kind of the symbol
public var kind: SymbolKind?

public init(
name: String?,
containerName: String? = nil,
usr: String?,
bestLocalDeclaration: Location? = nil)
bestLocalDeclaration: Location? = nil,
kind: SymbolKind? = nil)
{
self.name = name
self.containerName = containerName
self.usr = usr
self.bestLocalDeclaration = bestLocalDeclaration
self.kind = kind
}
}
5 changes: 5 additions & 0 deletions Sources/SKSupport/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ extension AbsolutePath {
}
}
}

/// The directory to write generated module interfaces
public var defaultDirectoryForGeneratedInterfaces: AbsolutePath {
return AbsolutePath(NSTemporaryDirectory()).appending(component: "GeneratedInterfaces")
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import lib
import /*lib:import*/lib

Lib() . /*Lib.foo:call*/foo()
4 changes: 4 additions & 0 deletions Sources/SourceKitD/SourceKitD.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ extension SourceKitD {

return handle
}

public func cancel(_ handle: sourcekitd_request_handle_t) {
api.cancel_request(handle)
}
}

private func logRequest(_ request: SKDRequestDictionary) {
Expand Down
6 changes: 6 additions & 0 deletions Sources/SourceKitD/sourcekitd_uids.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public struct sourcekitd_keys {
public let kind: sourcekitd_uid_t
public let length: sourcekitd_uid_t
public let line: sourcekitd_uid_t
public let modulename: sourcekitd_uid_t
public let name: sourcekitd_uid_t
public let namelength: sourcekitd_uid_t
public let nameoffset: sourcekitd_uid_t
Expand All @@ -63,6 +64,7 @@ public struct sourcekitd_keys {
public let substructure: sourcekitd_uid_t
public let syntactic_only: sourcekitd_uid_t
public let syntaxmap: sourcekitd_uid_t
public let synthesizedextensions: sourcekitd_uid_t
public let enablesyntaxmap: sourcekitd_uid_t
public let text: sourcekitd_uid_t
public let typename: sourcekitd_uid_t
Expand Down Expand Up @@ -118,6 +120,7 @@ public struct sourcekitd_keys {
kind = api.uid_get_from_cstr("key.kind")!
length = api.uid_get_from_cstr("key.length")!
line = api.uid_get_from_cstr("key.line")!
modulename = api.uid_get_from_cstr("key.modulename")!
name = api.uid_get_from_cstr("key.name")!
namelength = api.uid_get_from_cstr("key.namelength")!
nameoffset = api.uid_get_from_cstr("key.nameoffset")!
Expand All @@ -137,6 +140,7 @@ public struct sourcekitd_keys {
syntactic_only = api.uid_get_from_cstr("key.syntactic_only")!
syntaxmap = api.uid_get_from_cstr("key.syntaxmap")!
enablesyntaxmap = api.uid_get_from_cstr("key.enablesyntaxmap")!
synthesizedextensions = api.uid_get_from_cstr("key.synthesizedextensions")!
text = api.uid_get_from_cstr("key.text")!
typename = api.uid_get_from_cstr("key.typename")!
usr = api.uid_get_from_cstr("key.usr")!
Expand All @@ -163,6 +167,7 @@ public struct sourcekitd_keys {
public struct sourcekitd_requests {
public let crash_exit: sourcekitd_uid_t
public let editor_open: sourcekitd_uid_t
public let editor_open_interface: sourcekitd_uid_t
public let editor_close: sourcekitd_uid_t
public let editor_replacetext: sourcekitd_uid_t
public let codecomplete: sourcekitd_uid_t
Expand All @@ -178,6 +183,7 @@ public struct sourcekitd_requests {
public init(api: sourcekitd_functions_t) {
crash_exit = api.uid_get_from_cstr("source.request.crash_exit")!
editor_open = api.uid_get_from_cstr("source.request.editor.open")!
editor_open_interface = api.uid_get_from_cstr("source.request.editor.open.interface")!
editor_close = api.uid_get_from_cstr("source.request.editor.close")!
editor_replacetext = api.uid_get_from_cstr("source.request.editor.replacetext")!
codecomplete = api.uid_get_from_cstr("source.request.codecomplete")!
Expand Down
4 changes: 4 additions & 0 deletions Sources/SourceKitLSP/Clang/ClangLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,10 @@ extension ClangLanguageServerShim {
}
}

func openInterface(_ request: Request<OpenInterfaceRequest>) {
request.reply(.failure(.unknown("unsupported method")))
}

// MARK: - Other

func executeCommand(_ req: Request<ExecuteCommandRequest>) {
Expand Down
9 changes: 8 additions & 1 deletion Sources/SourceKitLSP/SourceKitServer+Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import LanguageServerProtocol
import SKCore
import struct TSCBasic.AbsolutePath
import SKSupport

extension SourceKitServer {

Expand All @@ -30,17 +32,22 @@ extension SourceKitServer {

/// Options for code-completion.
public var completionOptions: SKCompletionOptions

/// Override the default directory where generated interfaces will be stored
public var generatedInterfacesPath: AbsolutePath

public init(
buildSetup: BuildSetup = .default,
clangdOptions: [String] = [],
indexOptions: IndexOptions = .init(),
completionOptions: SKCompletionOptions = .init())
completionOptions: SKCompletionOptions = .init(),
generatedInterfacesPath: AbsolutePath = defaultDirectoryForGeneratedInterfaces)
{
self.buildSetup = buildSetup
self.clangdOptions = clangdOptions
self.indexOptions = indexOptions
self.completionOptions = completionOptions
self.generatedInterfacesPath = generatedInterfacesPath
}
}
}
29 changes: 29 additions & 0 deletions Sources/SourceKitLSP/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ public final class SourceKitServer: LanguageServer {
registerToolchainTextDocumentRequest(SourceKitServer.completion,
CompletionList(isIncomplete: false, items: []))
registerToolchainTextDocumentRequest(SourceKitServer.hover, nil)
registerToolchainTextDocumentRequest(SourceKitServer.openInterface, nil)
registerToolchainTextDocumentRequest(SourceKitServer.declaration, .locations([]))
registerToolchainTextDocumentRequest(SourceKitServer.definition, .locations([]))
registerToolchainTextDocumentRequest(SourceKitServer.references, [])
Expand Down Expand Up @@ -985,6 +986,14 @@ extension SourceKitServer {
) {
languageService.hover(req)
}

func openInterface(
_ req: Request<OpenInterfaceRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) {
languageService.openInterface(req)
}

/// Find all symbols in the workspace that include a string in their name.
/// - returns: An array of SymbolOccurrences that match the string.
Expand Down Expand Up @@ -1272,6 +1281,26 @@ extension SourceKitServer {
let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position)
let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index
let callback = callbackOnQueue(self.queue) { (result: LSPResult<SymbolInfoRequest.Response>) in

// If this symbol is a module then generate a textual interface
if case .success(let symbols) = result, let symbol = symbols.first, symbol.kind == .module, let name = symbol.name {
let openInterface = OpenInterfaceRequest(textDocument: req.params.textDocument, name: name)
let request = Request(openInterface, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: { (result: Result<OpenInterfaceRequest.Response, ResponseError>) in
switch result {
case .success(let interfaceDetails?):
let loc = Location(uri: interfaceDetails.uri, range: Range(Position(line: 0, utf16index: 0)))
req.reply(.locations([loc]))
case .success(nil):
req.reply(.failure(.unknown("Could not generate Swift Interface for \(name)")))
case .failure(let error):
req.reply(.failure(error))
}
})
languageService.openInterface(request)
return
}

let extractedResult = self.extractIndexedOccurrences(result: result, index: index, useLocalFallback: true) { (usr, index) in
log("performing indexed jump-to-def with usr \(usr)")
var occurs = index.occurrences(ofUSR: usr, roles: [.definition])
Expand Down
5 changes: 3 additions & 2 deletions Sources/SourceKitLSP/Swift/CursorInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ extension SwiftLanguageServer {
return completion(.failure(.responseError(ResponseError(result.failure!))))
}

guard let _: sourcekitd_uid_t = dict[keys.kind] else {
guard let kind: sourcekitd_uid_t = dict[keys.kind] else {
// Nothing to report.
return completion(.success(nil))
}
Expand All @@ -130,7 +130,8 @@ extension SwiftLanguageServer {
name: dict[keys.name],
containerName: nil,
usr: dict[keys.usr],
bestLocalDeclaration: location),
bestLocalDeclaration: location,
kind: kind.asSymbolKind(self.sourcekitd.values)),
annotatedDeclaration: dict[keys.annotated_decl],
documentationXML: dict[keys.doc_full_as_xml],
refactorActions:
Expand Down
84 changes: 84 additions & 0 deletions Sources/SourceKitLSP/Swift/OpenInterface.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
import SourceKitD
import LanguageServerProtocol
import LSPLogging

struct InterfaceInfo {
var contents: String
}

extension SwiftLanguageServer {
public func openInterface(_ request: LanguageServerProtocol.Request<LanguageServerProtocol.OpenInterfaceRequest>) {
let uri = request.params.textDocument.uri
let moduleName = request.params.name
self.queue.async {
let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(moduleName).swiftinterface")
let interfaceDocURI = DocumentURI(interfaceFilePath)
self._openInterface(request: request, uri: uri, name: moduleName, interfaceURI: interfaceDocURI) { result in
switch result {
case .success(let interfaceInfo):
do {
try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8)
request.reply(.success(InterfaceDetails(uri: interfaceDocURI)))
} catch {
request.reply(.failure(ResponseError.unknown(error.localizedDescription)))
}
case .failure(let error):
log("open interface failed: \(error)", level: .warning)
request.reply(.failure(ResponseError(error)))
}
}
}
}

/// Open the Swift interface for a module.
///
/// - Parameters:
/// - request: The OpenInterfaceRequest.
/// - uri: The document whose compiler arguments should be used to generate the interface.
/// - name: The name of the module whose interface should be generated.
/// - interfaceURI: The file where the generated interface should be written.
/// - completion: Completion block to asynchronously receive the InterfaceInfo, or error.
private func _openInterface(request: LanguageServerProtocol.Request<LanguageServerProtocol.OpenInterfaceRequest>,
uri: DocumentURI,
name: String,
interfaceURI: DocumentURI,
completion: @escaping (Swift.Result<InterfaceInfo, SKDError>) -> Void) {
let keys = self.keys
let skreq = SKDRequestDictionary(sourcekitd: sourcekitd)
skreq[keys.request] = requests.editor_open_interface
skreq[keys.modulename] = name
skreq[keys.name] = interfaceURI.pseudoPath
skreq[keys.synthesizedextensions] = 1
if let compileCommand = self.commandsByFile[uri] {
skreq[keys.compilerargs] = compileCommand.compilerArgs
}

let handle = self.sourcekitd.send(skreq, self.queue) { result in
switch result {
case .success(let dict):
return completion(.success(InterfaceInfo(contents: dict[keys.sourcetext] ?? "")))
case .failure(let error):
return completion(.failure(error))
}
}

if let handle = handle {
request.cancellationToken.addCancellationHandler { [weak self] in
self?.sourcekitd.cancel(handle)
}
}
}
}
11 changes: 9 additions & 2 deletions Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
//
//===----------------------------------------------------------------------===//

import Foundation
import Dispatch
import struct Foundation.CharacterSet
import LanguageServerProtocol
import LSPLogging
import SKCore
Expand Down Expand Up @@ -106,6 +106,9 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
let clientCapabilities: ClientCapabilities

let serverOptions: SourceKitServer.Options

/// Directory where generated Swift interfaces will be stored.
let generatedInterfacesPath: URL

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

public func canHandle(workspace: Workspace) -> Bool {
Expand Down Expand Up @@ -259,7 +264,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
}
})
}

/// Publish diagnostics for the given `snapshot`. We withhold semantic diagnostics if we are using
/// fallback arguments.
///
Expand Down Expand Up @@ -1576,6 +1581,8 @@ extension sourcekitd_uid_t {
case vals.decl_extension:
// There are no extensions in LSP, so I return something vaguely similar
return .namespace
case vals.ref_module:
return .module
default:
return nil
}
Expand Down
1 change: 1 addition & 0 deletions Sources/SourceKitLSP/ToolchainLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public protocol ToolchainLanguageServer: AnyObject {
func completion(_ req: Request<CompletionRequest>)
func hover(_ req: Request<HoverRequest>)
func symbolInfo(_ request: Request<SymbolInfoRequest>)
func openInterface(_ request: Request<OpenInterfaceRequest>)

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