diff --git a/Sources/LanguageServerProtocol/Messages.swift b/Sources/LanguageServerProtocol/Messages.swift index 583784f4a..38c4c1d32 100644 --- a/Sources/LanguageServerProtocol/Messages.swift +++ b/Sources/LanguageServerProtocol/Messages.swift @@ -56,6 +56,7 @@ public let builtinRequests: [_RequestType.Type] = [ InlineValueRequest.self, LinkedEditingRangeRequest.self, MonikersRequest.self, + OpenInterfaceRequest.self, PollIndexRequest.self, PrepareRenameRequest.self, ReferencesRequest.self, diff --git a/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift b/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift new file mode 100644 index 000000000..6292c1d9f --- /dev/null +++ b/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift @@ -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 + } +} diff --git a/Sources/LanguageServerProtocol/Requests/SymbolInfoRequest.swift b/Sources/LanguageServerProtocol/Requests/SymbolInfoRequest.swift index bf1865626..fb00dbb1f 100644 --- a/Sources/LanguageServerProtocol/Requests/SymbolInfoRequest.swift +++ b/Sources/LanguageServerProtocol/Requests/SymbolInfoRequest.swift @@ -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 } } diff --git a/Sources/SKSupport/FileSystem.swift b/Sources/SKSupport/FileSystem.swift index 361e64d0e..2182f672b 100644 --- a/Sources/SKSupport/FileSystem.swift +++ b/Sources/SKSupport/FileSystem.swift @@ -30,3 +30,8 @@ extension AbsolutePath { } } } + +/// The directory to write generated module interfaces +public var defaultDirectoryForGeneratedInterfaces: AbsolutePath { + return AbsolutePath(NSTemporaryDirectory()).appending(component: "GeneratedInterfaces") +} diff --git a/Sources/SKTestSupport/INPUTS/SwiftPMPackage/Sources/exec/main.swift b/Sources/SKTestSupport/INPUTS/SwiftPMPackage/Sources/exec/main.swift index 825671936..1ee2285e5 100644 --- a/Sources/SKTestSupport/INPUTS/SwiftPMPackage/Sources/exec/main.swift +++ b/Sources/SKTestSupport/INPUTS/SwiftPMPackage/Sources/exec/main.swift @@ -1,3 +1,3 @@ -import lib +import /*lib:import*/lib Lib() . /*Lib.foo:call*/foo() diff --git a/Sources/SourceKitD/SourceKitD.swift b/Sources/SourceKitD/SourceKitD.swift index af1bbfca2..fe0451675 100644 --- a/Sources/SourceKitD/SourceKitD.swift +++ b/Sources/SourceKitD/SourceKitD.swift @@ -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) { diff --git a/Sources/SourceKitD/sourcekitd_uids.swift b/Sources/SourceKitD/sourcekitd_uids.swift index 5023f6b3e..d34ba191c 100644 --- a/Sources/SourceKitD/sourcekitd_uids.swift +++ b/Sources/SourceKitD/sourcekitd_uids.swift @@ -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 @@ -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 @@ -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")! @@ -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")! @@ -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 @@ -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")! diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift index 0635162d4..91c0fc497 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift @@ -522,6 +522,10 @@ extension ClangLanguageServerShim { } } + func openInterface(_ request: Request) { + request.reply(.failure(.unknown("unsupported method"))) + } + // MARK: - Other func executeCommand(_ req: Request) { diff --git a/Sources/SourceKitLSP/SourceKitServer+Options.swift b/Sources/SourceKitLSP/SourceKitServer+Options.swift index 845e9c65f..5335907fa 100644 --- a/Sources/SourceKitLSP/SourceKitServer+Options.swift +++ b/Sources/SourceKitLSP/SourceKitServer+Options.swift @@ -12,6 +12,8 @@ import LanguageServerProtocol import SKCore +import struct TSCBasic.AbsolutePath +import SKSupport extension SourceKitServer { @@ -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 } } } diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index 12335a67b..487ab7eff 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -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, []) @@ -985,6 +986,14 @@ extension SourceKitServer { ) { languageService.hover(req) } + + func openInterface( + _ req: Request, + 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. @@ -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) 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) 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]) diff --git a/Sources/SourceKitLSP/Swift/CursorInfo.swift b/Sources/SourceKitLSP/Swift/CursorInfo.swift index 1bbc9d696..ae58d5e31 100644 --- a/Sources/SourceKitLSP/Swift/CursorInfo.swift +++ b/Sources/SourceKitLSP/Swift/CursorInfo.swift @@ -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)) } @@ -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: diff --git a/Sources/SourceKitLSP/Swift/OpenInterface.swift b/Sources/SourceKitLSP/Swift/OpenInterface.swift new file mode 100644 index 000000000..6e3be3887 --- /dev/null +++ b/Sources/SourceKitLSP/Swift/OpenInterface.swift @@ -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) { + 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, + uri: DocumentURI, + name: String, + interfaceURI: DocumentURI, + completion: @escaping (Swift.Result) -> 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) + } + } + } +} diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift index 9fbf0d6b5..844428ba6 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift @@ -10,8 +10,8 @@ // //===----------------------------------------------------------------------===// +import Foundation import Dispatch -import struct Foundation.CharacterSet import LanguageServerProtocol import LSPLogging import SKCore @@ -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 @@ -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 { @@ -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. /// @@ -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 } diff --git a/Sources/SourceKitLSP/ToolchainLanguageServer.swift b/Sources/SourceKitLSP/ToolchainLanguageServer.swift index cdfed3828..c98ed715f 100644 --- a/Sources/SourceKitLSP/ToolchainLanguageServer.swift +++ b/Sources/SourceKitLSP/ToolchainLanguageServer.swift @@ -81,6 +81,7 @@ public protocol ToolchainLanguageServer: AnyObject { func completion(_ req: Request) func hover(_ req: Request) func symbolInfo(_ request: Request) + func openInterface(_ request: Request) /// Returns true if the `ToolchainLanguageServer` will take ownership of the request. func definition(_ request: Request) -> Bool diff --git a/Sources/sourcekit-lsp/main.swift b/Sources/sourcekit-lsp/main.swift index a05198550..5a0735542 100644 --- a/Sources/sourcekit-lsp/main.swift +++ b/Sources/sourcekit-lsp/main.swift @@ -130,6 +130,10 @@ struct Main: ParsableCommand { ) var indexPrefixMappings = [PathPrefixMapping]() + @Option( + help: "Specify the directory where generated interfaces will be stored" + ) + var generatedInterfacesPath = defaultDirectoryForGeneratedInterfaces @Option( help: "Whether to enable server-side filtering in code-completion" @@ -156,6 +160,7 @@ struct Main: ParsableCommand { serverOptions.indexOptions.indexPrefixMappings = indexPrefixMappings serverOptions.completionOptions.serverSideFiltering = completionServerSideFiltering serverOptions.completionOptions.maxResults = completionMaxResults + serverOptions.generatedInterfacesPath = generatedInterfacesPath return serverOptions } diff --git a/Tests/SourceKitLSPTests/LocalSwiftTests.swift b/Tests/SourceKitLSPTests/LocalSwiftTests.swift index 665ea9126..0d17b9598 100644 --- a/Tests/SourceKitLSPTests/LocalSwiftTests.swift +++ b/Tests/SourceKitLSPTests/LocalSwiftTests.swift @@ -1091,6 +1091,7 @@ final class LocalSwiftTests: XCTestCase { language: .swift, version: 1, text: """ + import Foundation struct S { func foo() { var local = 1 @@ -1103,13 +1104,30 @@ final class LocalSwiftTests: XCTestCase { textDocument: TextDocumentIdentifier(url), position: Position(line: 0, utf16index: 7))) + XCTAssertEqual(resp.count, 1) + if let sym = resp.first { + XCTAssertEqual(sym.name, "Foundation") + XCTAssertNil(sym.containerName) + XCTAssertEqual(sym.usr, nil) + XCTAssertEqual(sym.kind, .module) + XCTAssertEqual(sym.bestLocalDeclaration?.uri, nil) + XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, nil) + XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.utf16index, nil) + } + } + + do { + let resp = try! sk.sendSync(SymbolInfoRequest( + textDocument: TextDocumentIdentifier(url), + position: Position(line: 1, utf16index: 7))) + XCTAssertEqual(resp.count, 1) if let sym = resp.first { XCTAssertEqual(sym.name, "S") XCTAssertNil(sym.containerName) XCTAssertEqual(sym.usr, "s:1a1SV") XCTAssertEqual(sym.bestLocalDeclaration?.uri, uri) - XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 0) + XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 1) XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.utf16index, 7) } } @@ -1117,7 +1135,7 @@ final class LocalSwiftTests: XCTestCase { do { let resp = try! sk.sendSync(SymbolInfoRequest( textDocument: TextDocumentIdentifier(url), - position: Position(line: 1, utf16index: 7))) + position: Position(line: 2, utf16index: 7))) XCTAssertEqual(resp.count, 1) if let sym = resp.first { @@ -1125,7 +1143,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertNil(sym.containerName) XCTAssertEqual(sym.usr, "s:1a1SV3fooyyF") XCTAssertEqual(sym.bestLocalDeclaration?.uri, uri) - XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 1) + XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 2) XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.utf16index, 7) } } @@ -1133,7 +1151,7 @@ final class LocalSwiftTests: XCTestCase { do { let resp = try! sk.sendSync(SymbolInfoRequest( textDocument: TextDocumentIdentifier(url), - position: Position(line: 2, utf16index: 8))) + position: Position(line: 3, utf16index: 8))) XCTAssertEqual(resp.count, 1) if let sym = resp.first { @@ -1141,7 +1159,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertNil(sym.containerName) XCTAssertEqual(sym.usr, "s:1a1SV3fooyyF5localL_Sivp") XCTAssertEqual(sym.bestLocalDeclaration?.uri, uri) - XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 2) + XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 3) XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.utf16index, 8) } } @@ -1272,7 +1290,7 @@ final class LocalSwiftTests: XCTestCase { } } } - + func testDocumentSymbolHighlight() throws { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let uri = DocumentURI(url) diff --git a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift new file mode 100644 index 000000000..3be32e189 --- /dev/null +++ b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift @@ -0,0 +1,132 @@ +//===----------------------------------------------------------------------===// +// +// 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 LanguageServerProtocol +import LSPTestSupport +import SKTestSupport +import SourceKitLSP +import XCTest + +final class SwiftInterfaceTests: XCTestCase { + + /// Connection and lifetime management for the service. + var connection: TestSourceKitServer! = nil + + /// The primary interface to make requests to the SourceKitServer. + var sk: TestClient! = nil + + var documentManager: DocumentManager! { + connection.server!._documentManager + } + + override func setUp() { + connection = TestSourceKitServer() + sk = connection.client + _ = try! sk.sendSync(InitializeRequest( + processId: nil, + rootPath: nil, + rootURI: nil, + initializationOptions: nil, + capabilities: ClientCapabilities(workspace: nil, + textDocument: TextDocumentClientCapabilities( + codeAction: .init( + codeActionLiteralSupport: .init( + codeActionKind: .init(valueSet: [.quickFix]) + )), + publishDiagnostics: .init(codeDescriptionSupport: true) + )), + trace: .off, + workspaceFolders: nil)) + } + + override func tearDown() { + sk = nil + connection = nil + } + + func testSystemModuleInterface() throws { + let url = URL(fileURLWithPath: "/\(UUID())/a.swift") + let uri = DocumentURI(url) + + sk.send(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( + uri: uri, + language: .swift, + version: 1, + text: """ + import Foundation + """))) + + let _resp = try sk.sendSync(DefinitionRequest( + textDocument: TextDocumentIdentifier(url), + position: Position(line: 0, utf16index: 10))) + let resp = try XCTUnwrap(_resp) + guard case .locations(let locations) = resp else { + XCTFail("Unexpected response: \(resp)") + return + } + XCTAssertEqual(locations.count, 1) + let location = try XCTUnwrap(locations.first) + XCTAssertTrue(location.uri.pseudoPath.hasSuffix("/Foundation.swiftinterface")) + let fileContents = try XCTUnwrap(location.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) })) + // Sanity-check that the generated Swift Interface contains Swift code + XCTAssertTrue(fileContents.hasPrefix("import ")) + } + + func testOpenInterface() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } + try ws.buildAndIndex() + let importedModule = ws.testLoc("lib:import") + try ws.openDocument(importedModule.url, language: .swift) + let openInterface = OpenInterfaceRequest(textDocument: importedModule.docIdentifier, name: "lib") + let interfaceDetails = try XCTUnwrap(ws.sk.sendSync(openInterface)) + XCTAssertTrue(interfaceDetails.uri.pseudoPath.hasSuffix("/lib.swiftinterface")) + let fileContents = try XCTUnwrap(interfaceDetails.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) })) + XCTAssertTrue(fileContents.contains(""" + public struct Lib { + + public func foo() + + public init() + } + """)) + } + + func testSwiftInterfaceAcrossModules() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } + try ws.buildAndIndex() + let importedModule = ws.testLoc("lib:import") + try ws.openDocument(importedModule.url, language: .swift) + let _resp = try withExtendedLifetime(ws) { + try ws.sk.sendSync(DefinitionRequest( + textDocument: importedModule.docIdentifier, + position: importedModule.position)) + } + let resp = try XCTUnwrap(_resp) + guard case .locations(let locations) = resp else { + XCTFail("Unexpected response: \(resp)") + return + } + XCTAssertEqual(locations.count, 1) + let location = try XCTUnwrap(locations.first) + XCTAssertTrue(location.uri.pseudoPath.hasSuffix("/lib.swiftinterface")) + let fileContents = try XCTUnwrap(location.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) })) + XCTAssertTrue(fileContents.contains(""" + public struct Lib { + + public func foo() + + public init() + } + """)) + } +}