diff --git a/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift b/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift index 6292c1d9f..aa9a1b2e4 100644 --- a/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift +++ b/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift @@ -20,11 +20,30 @@ public struct OpenInterfaceRequest: TextDocumentRequest, Hashable { public var textDocument: TextDocumentIdentifier /// The module to generate an index for. - public var name: String + public var moduleName: String - public init(textDocument: TextDocumentIdentifier, name: String) { + /// The module group name. + public var groupNames: [String] + + /// The symbol USR to search for in the generated module interface. + public var symbolUSR: String? + + public init(textDocument: TextDocumentIdentifier, name: String, symbolUSR: String?) { self.textDocument = textDocument - self.name = name + self.symbolUSR = symbolUSR + // Stdlib Swift modules are all in the "Swift" module, but their symbols return a module name `Swift.***`. + let splitName = name.split(separator: ".") + self.moduleName = String(splitName[0]) + self.groupNames = [String.SubSequence](splitName.dropFirst()).map(String.init) + } + + /// Name of interface module name with group names appended + public var name: String { + if groupNames.count > 0 { + return "\(self.moduleName).\(self.groupNames.joined(separator: "."))" + } else { + return self.moduleName + } } } @@ -32,8 +51,10 @@ public struct OpenInterfaceRequest: TextDocumentRequest, Hashable { public struct InterfaceDetails: ResponseType, Hashable { public var uri: DocumentURI + public var position: Position? - public init(uri: DocumentURI) { + public init(uri: DocumentURI, position: Position?) { self.uri = uri + self.position = position } } diff --git a/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Package.swift b/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Package.swift new file mode 100644 index 000000000..c93201271 --- /dev/null +++ b/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version:5.1 + +import PackageDescription + +let package = Package( + name: "SystemSwiftInterface", + platforms: [.macOS(.v10_15)], + products: [], + dependencies: [], + targets: [ + .target( + name: "lib", + dependencies: []), + /*Package.swift:targets*/ + ] +) diff --git a/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Sources/lib/lib.swift b/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Sources/lib/lib.swift new file mode 100644 index 000000000..2acbd3c78 --- /dev/null +++ b/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Sources/lib/lib.swift @@ -0,0 +1,10 @@ +public func libFunc() async { + let a: /*lib.string*/String = "test" + let i: /*lib.integer*/Int = 2 + await /*lib.withTaskGroup*/withTaskGroup(of: Void.self) { group in + group.addTask { + print(a) + print(i) + } + } +} \ No newline at end of file diff --git a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift index bcd54c896..f5b97dd00 100644 --- a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift +++ b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift @@ -120,20 +120,25 @@ extension SKSwiftPMTestWorkspace { public func testLoc(_ name: String) -> TestLocation { sources.locations[name]! } - public func buildAndIndex() throws { - try build() + public func buildAndIndex(withSystemSymbols: Bool = false) throws { + try build(withSystemSymbols: withSystemSymbols) index.pollForUnitChangesAndWait() } - func build() throws { - try TSCBasic.Process.checkNonZeroExit(arguments: [ + func build(withSystemSymbols: Bool = false) throws { + var arguments = [ toolchain.swift!.pathString, "build", "--package-path", sources.rootDirectory.path, "--scratch-path", buildDir.path, - "-Xswiftc", "-index-ignore-system-modules", - "-Xcc", "-index-ignore-system-symbols", - ]) + ] + if !withSystemSymbols { + arguments.append(contentsOf: [ + "-Xswiftc", "-index-ignore-system-modules", + "-Xcc", "-index-ignore-system-symbols", + ]) + } + try TSCBasic.Process.checkNonZeroExit(arguments: arguments) } } diff --git a/Sources/SourceKitD/sourcekitd_uids.swift b/Sources/SourceKitD/sourcekitd_uids.swift index d34ba191c..b88dd9390 100644 --- a/Sources/SourceKitD/sourcekitd_uids.swift +++ b/Sources/SourceKitD/sourcekitd_uids.swift @@ -40,6 +40,7 @@ public struct sourcekitd_keys { public let expression_type_list: sourcekitd_uid_t public let filepath: sourcekitd_uid_t public let fixits: sourcekitd_uid_t + public let groupname: sourcekitd_uid_t public let id: sourcekitd_uid_t public let is_system: sourcekitd_uid_t public let kind: sourcekitd_uid_t @@ -115,6 +116,7 @@ public struct sourcekitd_keys { expression_type_list = api.uid_get_from_cstr("key.expression_type_list")! filepath = api.uid_get_from_cstr("key.filepath")! fixits = api.uid_get_from_cstr("key.fixits")! + groupname = api.uid_get_from_cstr("key.groupname")! id = api.uid_get_from_cstr("key.id")! is_system = api.uid_get_from_cstr("key.is_system")! kind = api.uid_get_from_cstr("key.kind")! @@ -176,6 +178,7 @@ public struct sourcekitd_requests { public let codecomplete_close: sourcekitd_uid_t public let cursorinfo: sourcekitd_uid_t public let expression_type: sourcekitd_uid_t + public let find_usr: sourcekitd_uid_t public let variable_type: sourcekitd_uid_t public let relatedidents: sourcekitd_uid_t public let semantic_refactoring: sourcekitd_uid_t @@ -192,6 +195,7 @@ public struct sourcekitd_requests { codecomplete_close = api.uid_get_from_cstr("source.request.codecomplete.close")! cursorinfo = api.uid_get_from_cstr("source.request.cursorinfo")! expression_type = api.uid_get_from_cstr("source.request.expression.type")! + find_usr = api.uid_get_from_cstr("source.request.editor.find_usr")! variable_type = api.uid_get_from_cstr("source.request.variable.type")! relatedidents = api.uid_get_from_cstr("source.request.relatedidents")! semantic_refactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")! diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index 9e9156094..7a0f69c36 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -1307,20 +1307,7 @@ extension SourceKitServer { // 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) + self.respondWithInterface(req, moduleName: name, symbolUSR: nil, languageService: languageService) return } @@ -1335,6 +1322,19 @@ extension SourceKitServer { switch extractedResult { case .success(let resolved): + // if first resolved location is in `.swiftinterface` file. Use moduleName to return + // textual interface + if let firstResolved = resolved.first, + let moduleName = firstResolved.occurrence?.location.moduleName, + firstResolved.location.uri.fileURL?.pathExtension == "swiftinterface" { + self.respondWithInterface( + req, + moduleName: moduleName, + symbolUSR: firstResolved.occurrence?.symbol.usr, + languageService: languageService + ) + return + } let locs = resolved.map(\.location) // If we're unable to handle the definition request using our index, see if the // language service can handle it (e.g. clangd can provide AST based definitions). @@ -1354,6 +1354,29 @@ extension SourceKitServer { languageService.symbolInfo(request) } + func respondWithInterface( + _ req: Request, + moduleName: String, + symbolUSR: String?, + languageService: ToolchainLanguageServer + ) { + let openInterface = OpenInterfaceRequest(textDocument: req.params.textDocument, name: moduleName, symbolUSR: symbolUSR) + let request = Request(openInterface, id: req.id, clientID: ObjectIdentifier(self), + cancellation: req.cancellationToken, reply: { (result: Result) in + switch result { + case .success(let interfaceDetails?): + let position = interfaceDetails.position ?? Position(line: 0, utf16index: 0) + let loc = Location(uri: interfaceDetails.uri, range: Range(position)) + req.reply(.locations([loc])) + case .success(nil): + req.reply(.failure(.unknown("Could not generate Swift Interface for \(moduleName)"))) + case .failure(let error): + req.reply(.failure(error)) + } + }) + languageService.openInterface(request) + } + func implementation( _ req: Request, workspace: Workspace, diff --git a/Sources/SourceKitLSP/Swift/OpenInterface.swift b/Sources/SourceKitLSP/Swift/OpenInterface.swift index 6e3be3887..a35391416 100644 --- a/Sources/SourceKitLSP/Swift/OpenInterface.swift +++ b/Sources/SourceKitLSP/Swift/OpenInterface.swift @@ -14,30 +14,46 @@ import Foundation import SourceKitD import LanguageServerProtocol import LSPLogging +import SKSupport struct InterfaceInfo { var contents: String } +struct FindUSRInfo { + let position: Position? +} + extension SwiftLanguageServer { public func openInterface(_ request: LanguageServerProtocol.Request) { let uri = request.params.textDocument.uri - let moduleName = request.params.name + let moduleName = request.params.moduleName + let name = request.params.name + let symbol = request.params.symbolUSR self.queue.async { - let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(moduleName).swiftinterface") + let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(name).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))) + // has interface already been generated + if let snapshot = self.documentManager.latestSnapshot(interfaceDocURI) { + self._findUSRAndRespond(request: request, uri: interfaceDocURI, snapshot: snapshot, symbol: symbol) + } else { + // generate interface + self._openInterface(request: request, uri: uri, name: moduleName, interfaceURI: interfaceDocURI) { result in + switch result { + case .success(let interfaceInfo): + do { + // write to file + try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8) + // store snapshot + let snapshot = try self.documentManager.open(interfaceDocURI, language: .swift, version: 0, text: interfaceInfo.contents) + self._findUSRAndRespond(request: request, uri: interfaceDocURI, snapshot: snapshot, symbol: symbol) + } catch { + request.reply(.failure(ResponseError.unknown(error.localizedDescription))) + } + case .failure(let error): + log("open interface failed: \(error)", level: .warning) + request.reply(.failure(ResponseError(error))) } - case .failure(let error): - log("open interface failed: \(error)", level: .warning) - request.reply(.failure(ResponseError(error))) } } } @@ -60,6 +76,9 @@ extension SwiftLanguageServer { let skreq = SKDRequestDictionary(sourcekitd: sourcekitd) skreq[keys.request] = requests.editor_open_interface skreq[keys.modulename] = name + if request.params.groupNames.count > 0 { + skreq[keys.groupname] = request.params.groupNames + } skreq[keys.name] = interfaceURI.pseudoPath skreq[keys.synthesizedextensions] = 1 if let compileCommand = self.commandsByFile[uri] { @@ -81,4 +100,58 @@ extension SwiftLanguageServer { } } } + + private func _findUSRAndRespond( + request: LanguageServerProtocol.Request, + uri: DocumentURI, + snapshot: DocumentSnapshot, + symbol: String? + ) { + self._findUSR(request: request, uri: uri, snapshot: snapshot, symbol: symbol) { result in + switch result { + case .success(let info): + request.reply(.success(InterfaceDetails(uri: uri, position: info.position))) + case .failure: + request.reply(.success(InterfaceDetails(uri: uri, position: nil))) + } + } + } + + private func _findUSR( + request: LanguageServerProtocol.Request, + uri: DocumentURI, + snapshot: DocumentSnapshot, + symbol: String?, + completion: @escaping (Swift.Result) -> Void + ) { + guard let symbol = symbol else { + return completion(.success(FindUSRInfo(position: nil))) + } + let keys = self.keys + let skreq = SKDRequestDictionary(sourcekitd: sourcekitd) + + skreq[keys.request] = requests.find_usr + skreq[keys.sourcefile] = uri.pseudoPath + skreq[keys.usr] = symbol + + let handle = self.sourcekitd.send(skreq, self.queue) { result in + switch result { + case .success(let dict): + if let offset: Int = dict[keys.offset], + let position = snapshot.positionOf(utf8Offset: offset) { + return completion(.success(FindUSRInfo(position: position))) + } else { + return completion(.success(FindUSRInfo(position: nil))) + } + 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/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift index 513756cdc..25850fd79 100644 --- a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift +++ b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift @@ -11,9 +11,11 @@ //===----------------------------------------------------------------------===// import Foundation +import ISDBTestSupport import LanguageServerProtocol import LSPTestSupport import LSPLogging +import SKSupport import SKTestSupport import SourceKitLSP import XCTest @@ -99,7 +101,7 @@ final class SwiftInterfaceTests: XCTestCase { 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 openInterface = OpenInterfaceRequest(textDocument: importedModule.docIdentifier, name: "lib", symbolUSR: nil) 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) })) @@ -113,6 +115,61 @@ final class SwiftInterfaceTests: XCTestCase { """)) } + /// Used by testDefinitionInSystemModuleInterface + func testSystemSwiftInterface( + _ testLoc: TestLocation, + ws: SKSwiftPMTestWorkspace, + swiftInterfaceFile: String, + linePrefix: String + ) throws { + try ws.openDocument(testLoc.url, language: .swift) + let definition = try ws.sk.sendSync(DefinitionRequest( + textDocument: testLoc.docIdentifier, + position: testLoc.position)) + guard case .locations(let jump) = definition else { + XCTFail("Response is not locations") + return + } + let location = try XCTUnwrap(jump.first) + XCTAssertTrue(location.uri.pseudoPath.hasSuffix(swiftInterfaceFile)) + // load contents of swiftinterface + let contents = try XCTUnwrap(location.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) })) + let lineTable = LineTable(contents) + let line = lineTable[location.range.lowerBound.line] + XCTAssert(line.hasPrefix(linePrefix)) + ws.closeDocument(testLoc.url) + } + + func testDefinitionInSystemModuleInterface() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SystemSwiftInterface") else { return } + try ws.buildAndIndex(withSystemSymbols: true) + let stringRef = ws.testLoc("lib.string") + let intRef = ws.testLoc("lib.integer") + let withTaskGroupRef = ws.testLoc("lib.withTaskGroup") + + // Test stdlib with one submodule + try testSystemSwiftInterface( + stringRef, + ws: ws, + swiftInterfaceFile: "/Swift.String.swiftinterface", + linePrefix: "@frozen public struct String" + ) + // Test stdlib with two submodules + try testSystemSwiftInterface( + intRef, + ws: ws, + swiftInterfaceFile: "/Swift.Math.Integers.swiftinterface", + linePrefix: "@frozen public struct Int" + ) + // Test concurrency + try testSystemSwiftInterface( + withTaskGroupRef, + ws: ws, + swiftInterfaceFile: "/_Concurrency.swiftinterface", + linePrefix: "@inlinable public func withTaskGroup" + ) + } + func testSwiftInterfaceAcrossModules() throws { guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex()