mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
Merge pull request #1887 from ahoppen/generated-interface-reference-document
Support semantic functionality in generated interfaces if the client supports `getReferenceDocument`
This commit is contained in:
@@ -51,6 +51,7 @@ package struct IndexedSingleSwiftFileTestProject {
|
||||
/// - cleanUp: Whether to remove the temporary directory when the SourceKit-LSP server shuts down.
|
||||
package init(
|
||||
_ markedText: String,
|
||||
capabilities: ClientCapabilities = ClientCapabilities(),
|
||||
indexSystemModules: Bool = false,
|
||||
allowBuildFailure: Bool = false,
|
||||
workspaceDirectory: URL? = nil,
|
||||
@@ -153,6 +154,7 @@ package struct IndexedSingleSwiftFileTestProject {
|
||||
)
|
||||
self.testClient = try await TestSourceKitLSPClient(
|
||||
options: options,
|
||||
capabilities: capabilities,
|
||||
workspaceFolders: [
|
||||
WorkspaceFolder(uri: DocumentURI(testWorkspaceDirectory))
|
||||
],
|
||||
|
||||
@@ -48,11 +48,14 @@ target_sources(SourceKitLSP PRIVATE
|
||||
Swift/DocumentSymbols.swift
|
||||
Swift/ExpandMacroCommand.swift
|
||||
Swift/FoldingRange.swift
|
||||
Swift/GeneratedInterfaceDocumentURLData.swift
|
||||
Swift/GeneratedInterfaceManager.swift
|
||||
Swift/GeneratedInterfaceManager.swift
|
||||
Swift/MacroExpansion.swift
|
||||
Swift/MacroExpansionReferenceDocumentURLData.swift
|
||||
Swift/OpenInterface.swift
|
||||
Swift/RefactoringResponse.swift
|
||||
Swift/RefactoringEdit.swift
|
||||
Swift/RefactoringResponse.swift
|
||||
Swift/ReferenceDocumentURL.swift
|
||||
Swift/RelatedIdentifiers.swift
|
||||
Swift/RewriteSourceKitPlaceholders.swift
|
||||
|
||||
@@ -74,9 +74,9 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc
|
||||
case (.documentUpdate(let selfUri), .documentUpdate(let otherUri)):
|
||||
return selfUri == otherUri
|
||||
case (.documentUpdate(let selfUri), .documentRequest(let otherUri)):
|
||||
return selfUri == otherUri
|
||||
return selfUri.buildSettingsFile == otherUri.buildSettingsFile
|
||||
case (.documentRequest(let selfUri), .documentUpdate(let otherUri)):
|
||||
return selfUri == otherUri
|
||||
return selfUri.buildSettingsFile == otherUri.buildSettingsFile
|
||||
|
||||
// documentRequest
|
||||
case (.documentRequest, .documentRequest):
|
||||
|
||||
@@ -242,7 +242,7 @@ package actor SourceKitLSPServer {
|
||||
}
|
||||
|
||||
package func workspaceForDocument(uri: DocumentURI) async -> Workspace? {
|
||||
let uri = uri.primaryFile ?? uri
|
||||
let uri = uri.buildSettingsFile
|
||||
if let cachedWorkspace = self.workspaceForUri[uri]?.value {
|
||||
return cachedWorkspace
|
||||
}
|
||||
@@ -1592,14 +1592,14 @@ extension SourceKitLSPServer {
|
||||
}
|
||||
|
||||
func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse {
|
||||
let primaryFileURI = try ReferenceDocumentURL(from: req.uri).primaryFile
|
||||
let buildSettingsUri = try ReferenceDocumentURL(from: req.uri).buildSettingsFile
|
||||
|
||||
guard let workspace = await workspaceForDocument(uri: primaryFileURI) else {
|
||||
throw ResponseError.workspaceNotOpen(primaryFileURI)
|
||||
guard let workspace = await workspaceForDocument(uri: buildSettingsUri) else {
|
||||
throw ResponseError.workspaceNotOpen(buildSettingsUri)
|
||||
}
|
||||
|
||||
guard let languageService = workspace.documentService(for: primaryFileURI) else {
|
||||
throw ResponseError.unknown("No Language Service for URI: \(primaryFileURI)")
|
||||
guard let languageService = workspace.documentService(for: buildSettingsUri) else {
|
||||
throw ResponseError.unknown("No Language Service for URI: \(buildSettingsUri)")
|
||||
}
|
||||
|
||||
return try await languageService.getReferenceDocument(req)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2024 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
|
||||
|
||||
/// Represents url of generated interface reference document.
|
||||
|
||||
package struct GeneratedInterfaceDocumentURLData: Hashable, ReferenceURLData {
|
||||
package static let documentType = "generated-swift-interface"
|
||||
|
||||
private struct Parameters {
|
||||
static let moduleName = "moduleName"
|
||||
static let groupName = "groupName"
|
||||
static let sourcekitdDocumentName = "sourcekitdDocument"
|
||||
static let buildSettingsFrom = "buildSettingsFrom"
|
||||
}
|
||||
|
||||
/// The module that should be shown in this generated interface.
|
||||
let moduleName: String
|
||||
|
||||
/// The group that should be shown in this generated interface, if applicable.
|
||||
let groupName: String?
|
||||
|
||||
/// The name by which this document is referred to in sourcekitd.
|
||||
let sourcekitdDocumentName: String
|
||||
|
||||
/// The document from which the build settings for the generated interface should be inferred.
|
||||
let buildSettingsFrom: DocumentURI
|
||||
|
||||
var displayName: String {
|
||||
if let groupName {
|
||||
return "\(moduleName).\(groupName.replacing("/", with: ".")).swiftinterface"
|
||||
}
|
||||
return "\(moduleName).swiftinterface"
|
||||
}
|
||||
|
||||
var queryItems: [URLQueryItem] {
|
||||
var result = [
|
||||
URLQueryItem(name: Parameters.moduleName, value: moduleName)
|
||||
]
|
||||
if let groupName {
|
||||
result.append(URLQueryItem(name: Parameters.groupName, value: groupName))
|
||||
}
|
||||
result += [
|
||||
URLQueryItem(name: Parameters.sourcekitdDocumentName, value: sourcekitdDocumentName),
|
||||
URLQueryItem(name: Parameters.buildSettingsFrom, value: buildSettingsFrom.stringValue),
|
||||
]
|
||||
return result
|
||||
}
|
||||
|
||||
var uri: DocumentURI {
|
||||
get throws {
|
||||
try ReferenceDocumentURL.generatedInterface(self).uri
|
||||
}
|
||||
}
|
||||
|
||||
init(moduleName: String, groupName: String?, sourcekitdDocumentName: String, primaryFile: DocumentURI) {
|
||||
self.moduleName = moduleName
|
||||
self.groupName = groupName
|
||||
self.sourcekitdDocumentName = sourcekitdDocumentName
|
||||
self.buildSettingsFrom = primaryFile
|
||||
}
|
||||
|
||||
init(queryItems: [URLQueryItem]) throws {
|
||||
guard let moduleName = queryItems.last(where: { $0.name == Parameters.moduleName })?.value,
|
||||
let sourcekitdDocumentName = queryItems.last(where: { $0.name == Parameters.sourcekitdDocumentName })?.value,
|
||||
let primaryFile = queryItems.last(where: { $0.name == Parameters.buildSettingsFrom })?.value
|
||||
else {
|
||||
throw ReferenceDocumentURLError(description: "Invalid queryItems for generated interface reference document url")
|
||||
}
|
||||
|
||||
self.moduleName = moduleName
|
||||
self.groupName = queryItems.last(where: { $0.name == Parameters.groupName })?.value
|
||||
self.sourcekitdDocumentName = sourcekitdDocumentName
|
||||
self.buildSettingsFrom = try DocumentURI(string: primaryFile)
|
||||
}
|
||||
}
|
||||
233
Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift
Normal file
233
Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift
Normal file
@@ -0,0 +1,233 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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 LanguageServerProtocol
|
||||
import SKLogging
|
||||
import SKUtilities
|
||||
import SourceKitD
|
||||
import SwiftExtensions
|
||||
|
||||
/// When information about a generated interface is requested, this opens the generated interface in sourcekitd and
|
||||
/// caches the generated interface contents.
|
||||
///
|
||||
/// It keeps the generated interface open in sourcekitd until the corresponding reference document is closed in the
|
||||
/// editor. Additionally, it also keeps a few recently requested interfaces cached. This way we don't need to recompute
|
||||
/// the generated interface contents between the initial generated interface request to find a USR's position in the
|
||||
/// interface until the editor actually opens the reference document.
|
||||
actor GeneratedInterfaceManager {
|
||||
private struct OpenGeneratedInterfaceDocumentDetails {
|
||||
let url: GeneratedInterfaceDocumentURLData
|
||||
|
||||
/// The contents of the generated interface.
|
||||
let snapshot: DocumentSnapshot
|
||||
|
||||
/// The number of `GeneratedInterfaceManager` that are actively working with the sourcekitd document. If this value
|
||||
/// is 0, the generated interface may be closed in sourcekitd.
|
||||
///
|
||||
/// Usually, this value is 1, while the reference document for this generated interface is open in the editor.
|
||||
var refCount: Int
|
||||
}
|
||||
|
||||
private weak var swiftLanguageService: SwiftLanguageService?
|
||||
|
||||
/// The number of generated interface documents that are not in editor but should still be cached.
|
||||
private let cacheSize = 2
|
||||
|
||||
/// Details about the generated interfaces that are currently open in sourcekitd.
|
||||
///
|
||||
/// Conceptually, this is a dictionary with `url` being the key. To prevent excessive memory usage we only keep
|
||||
/// `cacheSize` entries with a ref count of 0 in the array. Older entries are at the end of the list, newer entries
|
||||
/// at the front.
|
||||
private var openInterfaces: [OpenGeneratedInterfaceDocumentDetails] = []
|
||||
|
||||
init(swiftLanguageService: SwiftLanguageService) {
|
||||
self.swiftLanguageService = swiftLanguageService
|
||||
}
|
||||
|
||||
/// If there are more than `cacheSize` entries in `openInterfaces` that have a ref count of 0, close the oldest ones.
|
||||
private func purgeCache() {
|
||||
var documentsToClose: [String] = []
|
||||
while openInterfaces.count(where: { $0.refCount == 0 }) > cacheSize,
|
||||
let indexToPurge = openInterfaces.lastIndex(where: { $0.refCount == 0 })
|
||||
{
|
||||
documentsToClose.append(openInterfaces[indexToPurge].url.sourcekitdDocumentName)
|
||||
openInterfaces.remove(at: indexToPurge)
|
||||
}
|
||||
if !documentsToClose.isEmpty, let swiftLanguageService {
|
||||
Task {
|
||||
let sourcekitd = swiftLanguageService.sourcekitd
|
||||
for documentToClose in documentsToClose {
|
||||
await orLog("Closing generated interface") {
|
||||
_ = try await swiftLanguageService.sendSourcekitdRequest(
|
||||
sourcekitd.dictionary([
|
||||
sourcekitd.keys.request: sourcekitd.requests.editorClose,
|
||||
sourcekitd.keys.name: documentToClose,
|
||||
sourcekitd.keys.cancelBuilds: 0,
|
||||
]),
|
||||
fileContents: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If we don't have the generated interface for the given `document` open in sourcekitd, open it, otherwise return
|
||||
/// its details from the cache.
|
||||
///
|
||||
/// If `incrementingRefCount` is `true`, then the document manager will keep the generated interface open in
|
||||
/// sourcekitd, independent of the cache size. If `incrementingRefCount` is `true`, then `decrementRefCount` must be
|
||||
/// called to allow the document to be closed again.
|
||||
private func details(
|
||||
for document: GeneratedInterfaceDocumentURLData,
|
||||
incrementingRefCount: Bool
|
||||
) async throws -> OpenGeneratedInterfaceDocumentDetails {
|
||||
func loadFromCache() -> OpenGeneratedInterfaceDocumentDetails? {
|
||||
guard let cachedIndex = openInterfaces.firstIndex(where: { $0.url == document }) else {
|
||||
return nil
|
||||
}
|
||||
if incrementingRefCount {
|
||||
openInterfaces[cachedIndex].refCount += 1
|
||||
}
|
||||
return openInterfaces[cachedIndex]
|
||||
|
||||
}
|
||||
if let cached = loadFromCache() {
|
||||
return cached
|
||||
}
|
||||
|
||||
guard let swiftLanguageService else {
|
||||
// `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do.
|
||||
throw ResponseError.unknown("Connection to the editor closed")
|
||||
}
|
||||
|
||||
let sourcekitd = swiftLanguageService.sourcekitd
|
||||
|
||||
let keys = sourcekitd.keys
|
||||
let skreq = sourcekitd.dictionary([
|
||||
keys.request: sourcekitd.requests.editorOpenInterface,
|
||||
keys.moduleName: document.moduleName,
|
||||
keys.groupName: document.groupName,
|
||||
keys.name: document.sourcekitdDocumentName,
|
||||
keys.synthesizedExtension: 1,
|
||||
keys.compilerArgs: await swiftLanguageService.buildSettings(for: try document.uri, fallbackAfterTimeout: false)?
|
||||
.compilerArgs as [SKDRequestValue]?,
|
||||
])
|
||||
|
||||
let dict = try await swiftLanguageService.sendSourcekitdRequest(skreq, fileContents: nil)
|
||||
|
||||
guard let contents: String = dict[keys.sourceText] else {
|
||||
throw ResponseError.unknown("sourcekitd response is missing sourceText")
|
||||
}
|
||||
|
||||
if let cached = loadFromCache() {
|
||||
// Another request raced us to create the generated interface. Discard what we computed here and return the cached
|
||||
// value.
|
||||
await orLog("Closing generated interface created during race") {
|
||||
_ = try await swiftLanguageService.sendSourcekitdRequest(
|
||||
sourcekitd.dictionary([
|
||||
keys.request: sourcekitd.requests.editorClose,
|
||||
keys.name: document.sourcekitdDocumentName,
|
||||
keys.cancelBuilds: 0,
|
||||
]),
|
||||
fileContents: nil
|
||||
)
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
let details = OpenGeneratedInterfaceDocumentDetails(
|
||||
url: document,
|
||||
snapshot: DocumentSnapshot(
|
||||
uri: try document.uri,
|
||||
language: .swift,
|
||||
version: 0,
|
||||
lineTable: LineTable(contents)
|
||||
),
|
||||
refCount: incrementingRefCount ? 1 : 0
|
||||
)
|
||||
openInterfaces.insert(details, at: 0)
|
||||
purgeCache()
|
||||
return details
|
||||
}
|
||||
|
||||
private func decrementRefCount(for document: GeneratedInterfaceDocumentURLData) {
|
||||
guard let cachedIndex = openInterfaces.firstIndex(where: { $0.url == document }) else {
|
||||
logger.fault(
|
||||
"Generated interface document for \(document.moduleName) is not open anymore. Unbalanced retain and releases?"
|
||||
)
|
||||
return
|
||||
}
|
||||
if openInterfaces[cachedIndex].refCount == 0 {
|
||||
logger.fault(
|
||||
"Generated interface document for \(document.moduleName) is already 0. Unbalanced retain and releases?"
|
||||
)
|
||||
return
|
||||
}
|
||||
openInterfaces[cachedIndex].refCount -= 1
|
||||
purgeCache()
|
||||
}
|
||||
|
||||
func position(ofUsr usr: String, in document: GeneratedInterfaceDocumentURLData) async throws -> Position {
|
||||
guard let swiftLanguageService else {
|
||||
// `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do.
|
||||
throw ResponseError.unknown("Connection to the editor closed")
|
||||
}
|
||||
|
||||
let details = try await details(for: document, incrementingRefCount: true)
|
||||
defer {
|
||||
decrementRefCount(for: document)
|
||||
}
|
||||
|
||||
let sourcekitd = swiftLanguageService.sourcekitd
|
||||
let keys = sourcekitd.keys
|
||||
let skreq = sourcekitd.dictionary([
|
||||
keys.request: sourcekitd.requests.editorFindUSR,
|
||||
keys.sourceFile: document.sourcekitdDocumentName,
|
||||
keys.usr: usr,
|
||||
])
|
||||
|
||||
let dict = try await swiftLanguageService.sendSourcekitdRequest(skreq, fileContents: details.snapshot.text)
|
||||
guard let offset: Int = dict[keys.offset] else {
|
||||
throw ResponseError.unknown("Missing key 'offset'")
|
||||
}
|
||||
return details.snapshot.positionOf(utf8Offset: offset)
|
||||
}
|
||||
|
||||
func snapshot(of document: GeneratedInterfaceDocumentURLData) async throws -> DocumentSnapshot {
|
||||
return try await details(for: document, incrementingRefCount: false).snapshot
|
||||
}
|
||||
|
||||
func open(document: GeneratedInterfaceDocumentURLData) async throws {
|
||||
_ = try await details(for: document, incrementingRefCount: true)
|
||||
}
|
||||
|
||||
func close(document: GeneratedInterfaceDocumentURLData) async {
|
||||
decrementRefCount(for: document)
|
||||
}
|
||||
|
||||
func reopen(interfacesWithBuildSettingsFrom buildSettingsFile: DocumentURI) async {
|
||||
for openInterface in openInterfaces {
|
||||
guard openInterface.url.buildSettingsFrom == buildSettingsFile else {
|
||||
continue
|
||||
}
|
||||
await orLog("Reopening generated interface") {
|
||||
// `MessageHandlingDependencyTracker` ensures that we don't handle a request for the generated interface while
|
||||
// it is being re-opened because `documentUpdate` and `documentRequest` use the `buildSettingsFile` to determine
|
||||
// their dependencies.
|
||||
await close(document: openInterface.url)
|
||||
openInterfaces.removeAll(where: { $0.url == openInterface.url })
|
||||
try await open(document: openInterface.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,8 @@ extension SwiftLanguageService {
|
||||
switch try? ReferenceDocumentURL(from: expandMacroCommand.textDocument.uri) {
|
||||
case .macroExpansion(let data):
|
||||
data.bufferName
|
||||
case .generatedInterface(let data):
|
||||
data.displayName
|
||||
case nil:
|
||||
expandMacroCommand.textDocument.uri.fileURL?.lastPathComponent ?? expandMacroCommand.textDocument.uri.pseudoPath
|
||||
}
|
||||
@@ -223,9 +225,7 @@ extension SwiftLanguageService {
|
||||
case .bool(true) = experimentalCapabilities["workspace/peekDocuments"],
|
||||
case .bool(true) = experimentalCapabilities["workspace/getReferenceDocument"]
|
||||
{
|
||||
let expansionURIs = try macroExpansionReferenceDocumentURLs.map {
|
||||
return DocumentURI(try $0.url)
|
||||
}
|
||||
let expansionURIs = try macroExpansionReferenceDocumentURLs.map { try $0.uri }
|
||||
|
||||
let uri = expandMacroCommand.textDocument.uri.primaryFile ?? expandMacroCommand.textDocument.uri
|
||||
|
||||
@@ -233,7 +233,7 @@ extension SwiftLanguageService {
|
||||
switch try? ReferenceDocumentURL(from: expandMacroCommand.textDocument.uri) {
|
||||
case .macroExpansion(let data):
|
||||
data.primaryFileSelectionRange.lowerBound
|
||||
case nil:
|
||||
case .generatedInterface, nil:
|
||||
expandMacroCommand.positionRange.lowerBound
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import RegexBuilder
|
||||
/// - `bufferName` denotes the buffer name of the specific macro expansion edit
|
||||
/// - `parent` denoting the URI of the document from which the macro was expanded. For a first-level macro expansion,
|
||||
/// this is a file URI. For nested macro expansions, this is a `sourcekit-lsp://swift-macro-expansion` URL.
|
||||
package struct MacroExpansionReferenceDocumentURLData {
|
||||
package struct MacroExpansionReferenceDocumentURLData: ReferenceURLData {
|
||||
package static let documentType = "swift-macro-expansion"
|
||||
|
||||
/// The document from which this macro was expanded. For first-level macro expansions, this is a file URL. For
|
||||
@@ -146,7 +146,7 @@ package struct MacroExpansionReferenceDocumentURLData {
|
||||
switch try? ReferenceDocumentURL(from: parent) {
|
||||
case .macroExpansion(let data):
|
||||
data.primaryFile
|
||||
case nil:
|
||||
case .generatedInterface, nil:
|
||||
parent
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ package struct MacroExpansionReferenceDocumentURLData {
|
||||
switch try? ReferenceDocumentURL(from: parent) {
|
||||
case .macroExpansion(let data):
|
||||
data.primaryFileSelectionRange
|
||||
case nil:
|
||||
case .generatedInterface, nil:
|
||||
self.parentSelectionRange
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,23 +10,14 @@
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if compiler(>=6)
|
||||
import Foundation
|
||||
package import LanguageServerProtocol
|
||||
import SKLogging
|
||||
import SKUtilities
|
||||
import SourceKitD
|
||||
#else
|
||||
import Foundation
|
||||
import LanguageServerProtocol
|
||||
import SKLogging
|
||||
import SKUtilities
|
||||
import SourceKitD
|
||||
#endif
|
||||
|
||||
struct GeneratedInterfaceInfo {
|
||||
var contents: String
|
||||
}
|
||||
#if compiler(>=6)
|
||||
package import LanguageServerProtocol
|
||||
#else
|
||||
import LanguageServerProtocol
|
||||
#endif
|
||||
|
||||
extension SwiftLanguageService {
|
||||
package func openGeneratedInterface(
|
||||
@@ -35,104 +26,35 @@ extension SwiftLanguageService {
|
||||
groupName: String?,
|
||||
symbolUSR symbol: String?
|
||||
) async throws -> GeneratedInterfaceDetails? {
|
||||
// Name of interface module name with group names appended
|
||||
let name =
|
||||
if let groupName {
|
||||
"\(moduleName).\(groupName.replacing("/", with: "."))"
|
||||
let urlData = GeneratedInterfaceDocumentURLData(
|
||||
moduleName: moduleName,
|
||||
groupName: groupName,
|
||||
sourcekitdDocumentName: "\(moduleName)-\(UUID())",
|
||||
primaryFile: document
|
||||
)
|
||||
let position: Position? =
|
||||
if let symbol {
|
||||
await orLog("Getting position of USR") {
|
||||
try await generatedInterfaceManager.position(ofUsr: symbol, in: urlData)
|
||||
}
|
||||
} else {
|
||||
moduleName
|
||||
nil
|
||||
}
|
||||
let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(name).swiftinterface")
|
||||
let interfaceDocURI = DocumentURI(interfaceFilePath)
|
||||
// has interface already been generated
|
||||
if let snapshot = try? await self.latestSnapshot(for: interfaceDocURI) {
|
||||
return await self.generatedInterfaceDetails(
|
||||
uri: interfaceDocURI,
|
||||
snapshot: snapshot,
|
||||
symbol: symbol
|
||||
)
|
||||
} else {
|
||||
let interfaceInfo = try await self.generatedInterfaceInfo(
|
||||
document: document,
|
||||
moduleName: moduleName,
|
||||
groupName: groupName,
|
||||
interfaceURI: interfaceDocURI
|
||||
)
|
||||
try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8)
|
||||
let snapshot = DocumentSnapshot(
|
||||
uri: interfaceDocURI,
|
||||
language: .swift,
|
||||
version: 0,
|
||||
lineTable: LineTable(interfaceInfo.contents)
|
||||
)
|
||||
let result = await self.generatedInterfaceDetails(
|
||||
uri: interfaceDocURI,
|
||||
snapshot: snapshot,
|
||||
symbol: symbol
|
||||
)
|
||||
_ = await orLog("Closing generated interface") {
|
||||
try await sendSourcekitdRequest(closeDocumentSourcekitdRequest(uri: interfaceDocURI), fileContents: nil)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the Swift interface for a module.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - document: The document whose compiler arguments should be used to generate the interface.
|
||||
/// - moduleName: The module to generate an index for.
|
||||
/// - groupName: The module group name.
|
||||
/// - interfaceURI: The file where the generated interface should be written.
|
||||
///
|
||||
/// - Important: This opens a document with name `interfaceURI.pseudoPath` in sourcekitd. The caller is responsible
|
||||
/// for ensuring that the document will eventually get closed in sourcekitd again.
|
||||
private func generatedInterfaceInfo(
|
||||
document: DocumentURI,
|
||||
moduleName: String,
|
||||
groupName: String?,
|
||||
interfaceURI: DocumentURI
|
||||
) async throws -> GeneratedInterfaceInfo {
|
||||
let keys = self.keys
|
||||
let skreq = sourcekitd.dictionary([
|
||||
keys.request: requests.editorOpenInterface,
|
||||
keys.moduleName: moduleName,
|
||||
keys.groupName: groupName,
|
||||
keys.name: interfaceURI.pseudoPath,
|
||||
keys.synthesizedExtension: 1,
|
||||
keys.compilerArgs: await self.buildSettings(for: document, fallbackAfterTimeout: false)?.compilerArgs
|
||||
as [SKDRequestValue]?,
|
||||
])
|
||||
|
||||
let dict = try await sendSourcekitdRequest(skreq, fileContents: nil)
|
||||
return GeneratedInterfaceInfo(contents: dict[keys.sourceText] ?? "")
|
||||
}
|
||||
|
||||
private func generatedInterfaceDetails(
|
||||
uri: DocumentURI,
|
||||
snapshot: DocumentSnapshot,
|
||||
symbol: String?
|
||||
) async -> GeneratedInterfaceDetails {
|
||||
do {
|
||||
guard let symbol = symbol else {
|
||||
return GeneratedInterfaceDetails(uri: uri, position: nil)
|
||||
}
|
||||
let keys = self.keys
|
||||
let skreq = sourcekitd.dictionary([
|
||||
keys.request: requests.editorFindUSR,
|
||||
keys.sourceFile: uri.sourcekitdSourceFile,
|
||||
keys.primaryFile: uri.primaryFile?.pseudoPath,
|
||||
keys.usr: symbol,
|
||||
])
|
||||
|
||||
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
|
||||
if let offset: Int = dict[keys.offset] {
|
||||
return GeneratedInterfaceDetails(uri: uri, position: snapshot.positionOf(utf8Offset: offset))
|
||||
} else {
|
||||
return GeneratedInterfaceDetails(uri: uri, position: nil)
|
||||
}
|
||||
} catch {
|
||||
return GeneratedInterfaceDetails(uri: uri, position: nil)
|
||||
|
||||
if case .dictionary(let experimentalCapabilities) = self.capabilityRegistry.clientCapabilities.experimental,
|
||||
case .bool(true) = experimentalCapabilities["workspace/getReferenceDocument"]
|
||||
{
|
||||
return GeneratedInterfaceDetails(uri: try urlData.uri, position: position)
|
||||
}
|
||||
let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent(urlData.displayName)
|
||||
try await generatedInterfaceManager.snapshot(of: urlData).text.write(
|
||||
to: interfaceFilePath,
|
||||
atomically: true,
|
||||
encoding: String.Encoding.utf8
|
||||
)
|
||||
return GeneratedInterfaceDetails(
|
||||
uri: DocumentURI(interfaceFilePath),
|
||||
position: position
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
import Foundation
|
||||
import LanguageServerProtocol
|
||||
|
||||
protocol ReferenceURLData {
|
||||
static var documentType: String { get }
|
||||
var displayName: String { get }
|
||||
var queryItems: [URLQueryItem] { get }
|
||||
}
|
||||
|
||||
/// A Reference Document is a document whose url scheme is `sourcekit-lsp:` and whose content can only be retrieved
|
||||
/// using `GetReferenceDocumentRequest`. The enum represents a specific type of reference document and its
|
||||
/// associated value represents the data necessary to generate the document's contents and its url
|
||||
@@ -28,25 +34,33 @@ package enum ReferenceDocumentURL {
|
||||
package static let scheme = "sourcekit-lsp"
|
||||
|
||||
case macroExpansion(MacroExpansionReferenceDocumentURLData)
|
||||
case generatedInterface(GeneratedInterfaceDocumentURLData)
|
||||
|
||||
var url: URL {
|
||||
get throws {
|
||||
switch self {
|
||||
case let .macroExpansion(data):
|
||||
var components = URLComponents()
|
||||
components.scheme = Self.scheme
|
||||
components.host = MacroExpansionReferenceDocumentURLData.documentType
|
||||
components.path = "/\(data.displayName)"
|
||||
components.queryItems = data.queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
throw ReferenceDocumentURLError(
|
||||
description: "Unable to create URL for macro expansion reference document"
|
||||
)
|
||||
let data: ReferenceURLData =
|
||||
switch self {
|
||||
case .macroExpansion(let data): data
|
||||
case .generatedInterface(let data): data
|
||||
}
|
||||
|
||||
return url
|
||||
var components = URLComponents()
|
||||
components.scheme = Self.scheme
|
||||
components.host = type(of: data).documentType
|
||||
components.path = "/\(data.displayName)"
|
||||
components.queryItems = data.queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
throw ReferenceDocumentURLError(description: "Unable to create URL for reference document")
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var uri: DocumentURI {
|
||||
get throws {
|
||||
DocumentURI(try url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +88,15 @@ package enum ReferenceDocumentURL {
|
||||
queryItems: queryItems
|
||||
)
|
||||
self = .macroExpansion(macroExpansionURLData)
|
||||
case GeneratedInterfaceDocumentURLData.documentType:
|
||||
guard let queryItems = URLComponents(string: url.absoluteString)?.queryItems else {
|
||||
throw ReferenceDocumentURLError(
|
||||
description: "No queryItems passed for generated interface reference document: \(url)"
|
||||
)
|
||||
}
|
||||
|
||||
let macroExpansionURLData = try GeneratedInterfaceDocumentURLData(queryItems: queryItems)
|
||||
self = .generatedInterface(macroExpansionURLData)
|
||||
case nil:
|
||||
throw ReferenceDocumentURLError(
|
||||
description: "Bad URL for reference document: \(url)"
|
||||
@@ -90,14 +113,23 @@ package enum ReferenceDocumentURL {
|
||||
/// For macro expansions, this is the buffer name that the URI references.
|
||||
var sourcekitdSourceFile: String {
|
||||
switch self {
|
||||
case let .macroExpansion(data): data.bufferName
|
||||
case .macroExpansion(let data): return data.bufferName
|
||||
case .generatedInterface(let data): return data.sourcekitdDocumentName
|
||||
}
|
||||
}
|
||||
|
||||
var primaryFile: DocumentURI {
|
||||
/// The file that should be used to retrieve build settings for this reference document.
|
||||
var buildSettingsFile: DocumentURI {
|
||||
switch self {
|
||||
case let .macroExpansion(data):
|
||||
return data.primaryFile
|
||||
case .macroExpansion(let data): return data.primaryFile
|
||||
case .generatedInterface(let data): return data.buildSettingsFrom
|
||||
}
|
||||
}
|
||||
|
||||
var primaryFile: DocumentURI? {
|
||||
switch self {
|
||||
case .macroExpansion(let data): return data.primaryFile
|
||||
case .generatedInterface(let data): return data.buildSettingsFrom.primaryFile
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +158,14 @@ extension DocumentURI {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// The file that should be used to retrieve build settings for this reference document.
|
||||
var buildSettingsFile: DocumentURI {
|
||||
if let referenceDocument = try? ReferenceDocumentURL(from: self) {
|
||||
return referenceDocument.buildSettingsFile
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
package struct ReferenceDocumentURLError: Error, CustomStringConvertible {
|
||||
|
||||
@@ -168,7 +168,22 @@ package actor SwiftLanguageService: LanguageService, Sendable {
|
||||
private let diagnosticReportManager: DiagnosticReportManager
|
||||
|
||||
/// - Note: Implicitly unwrapped optional so we can pass a reference of `self` to `MacroExpansionManager`.
|
||||
private(set) var macroExpansionManager: MacroExpansionManager!
|
||||
private(set) var macroExpansionManager: MacroExpansionManager! {
|
||||
willSet {
|
||||
// Must only be set once.
|
||||
precondition(macroExpansionManager == nil)
|
||||
precondition(newValue != nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Note: Implicitly unwrapped optional so we can pass a reference of `self` to `MacroExpansionManager`.
|
||||
private(set) var generatedInterfaceManager: GeneratedInterfaceManager! {
|
||||
willSet {
|
||||
// Must only be set once.
|
||||
precondition(generatedInterfaceManager == nil)
|
||||
precondition(newValue != nil)
|
||||
}
|
||||
}
|
||||
|
||||
var documentManager: DocumentManager {
|
||||
get throws {
|
||||
@@ -235,6 +250,7 @@ package actor SwiftLanguageService: LanguageService, Sendable {
|
||||
)
|
||||
|
||||
self.macroExpansionManager = MacroExpansionManager(swiftLanguageService: self)
|
||||
self.generatedInterfaceManager = GeneratedInterfaceManager(swiftLanguageService: self)
|
||||
|
||||
// Create sub-directories for each type of generated file
|
||||
try FileManager.default.createDirectory(at: generatedInterfacesPath, withIntermediateDirectories: true)
|
||||
@@ -252,23 +268,25 @@ package actor SwiftLanguageService: LanguageService, Sendable {
|
||||
case .macroExpansion(let data):
|
||||
let content = try await self.macroExpansionManager.macroExpansion(for: data)
|
||||
return DocumentSnapshot(uri: uri, language: .swift, version: 0, lineTable: LineTable(content))
|
||||
case .generatedInterface(let data):
|
||||
return try await self.generatedInterfaceManager.snapshot(of: data)
|
||||
case nil:
|
||||
return try documentManager.latestSnapshot(uri)
|
||||
}
|
||||
}
|
||||
|
||||
func buildSettings(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> SwiftCompileCommand? {
|
||||
let primaryDocument = document.primaryFile ?? document
|
||||
let buildSettingsFile = document.buildSettingsFile
|
||||
|
||||
guard let sourceKitLSPServer else {
|
||||
logger.fault("Cannot retrieve build settings because SourceKitLSPServer is no longer alive")
|
||||
return nil
|
||||
}
|
||||
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: primaryDocument) else {
|
||||
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: buildSettingsFile) else {
|
||||
return nil
|
||||
}
|
||||
if let settings = await workspace.buildSystemManager.buildSettingsInferredFromMainFile(
|
||||
for: primaryDocument,
|
||||
for: buildSettingsFile,
|
||||
language: .swift,
|
||||
fallbackAfterTimeout: fallbackAfterTimeout
|
||||
) {
|
||||
@@ -410,8 +428,10 @@ extension SwiftLanguageService {
|
||||
|
||||
package func reopenDocument(_ notification: ReopenTextDocumentNotification) async {
|
||||
switch try? ReferenceDocumentURL(from: notification.textDocument.uri) {
|
||||
case .macroExpansion:
|
||||
break
|
||||
case .macroExpansion, .generatedInterface:
|
||||
// Macro expansions and generated interfaces don't have document dependencies or build settings associated with
|
||||
// their URI. We should thus not not receive any `ReopenDocument` notifications for them.
|
||||
logger.fault("Unexpectedly received reopen document notification for reference document")
|
||||
case nil:
|
||||
let snapshot = orLog("Getting snapshot to re-open document") {
|
||||
try documentManager.latestSnapshot(notification.textDocument.uri)
|
||||
@@ -511,6 +531,10 @@ extension SwiftLanguageService {
|
||||
switch try? ReferenceDocumentURL(from: notification.textDocument.uri) {
|
||||
case .macroExpansion:
|
||||
break
|
||||
case .generatedInterface(let data):
|
||||
await orLog("Opening generated interface") {
|
||||
try await generatedInterfaceManager.open(document: data)
|
||||
}
|
||||
case nil:
|
||||
cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri)
|
||||
await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri)
|
||||
@@ -525,15 +549,16 @@ extension SwiftLanguageService {
|
||||
}
|
||||
|
||||
package func closeDocument(_ notification: DidCloseTextDocumentNotification) async {
|
||||
cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri)
|
||||
inFlightPublishDiagnosticsTasks[notification.textDocument.uri] = nil
|
||||
await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri)
|
||||
buildSettingsForOpenFiles[notification.textDocument.uri] = nil
|
||||
switch try? ReferenceDocumentURL(from: notification.textDocument.uri) {
|
||||
case .macroExpansion:
|
||||
break
|
||||
case .generatedInterface(let data):
|
||||
await generatedInterfaceManager.close(document: data)
|
||||
case nil:
|
||||
cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri)
|
||||
inFlightPublishDiagnosticsTasks[notification.textDocument.uri] = nil
|
||||
await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri)
|
||||
buildSettingsForOpenFiles[notification.textDocument.uri] = nil
|
||||
|
||||
let req = closeDocumentSourcekitdRequest(uri: notification.textDocument.uri)
|
||||
_ = try? await self.sendSourcekitdRequest(req, fileContents: nil)
|
||||
}
|
||||
@@ -830,6 +855,10 @@ extension SwiftLanguageService {
|
||||
}
|
||||
|
||||
package func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? {
|
||||
if (try? ReferenceDocumentURL(from: req.textDocument.uri)) != nil {
|
||||
// Do not show code actions in reference documents
|
||||
return nil
|
||||
}
|
||||
let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind?)] = [
|
||||
(retrieveSyntaxCodeActions, nil),
|
||||
(retrieveRefactorCodeActions, .refactor),
|
||||
@@ -1008,7 +1037,7 @@ extension SwiftLanguageService {
|
||||
package func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport {
|
||||
do {
|
||||
await semanticIndexManager?.prepareFileForEditorFunctionality(
|
||||
req.textDocument.uri.primaryFile ?? req.textDocument.uri
|
||||
req.textDocument.uri.buildSettingsFile
|
||||
)
|
||||
let snapshot = try await self.latestSnapshot(for: req.textDocument.uri)
|
||||
let buildSettings = await self.buildSettings(for: req.textDocument.uri, fallbackAfterTimeout: false)
|
||||
@@ -1061,6 +1090,10 @@ extension SwiftLanguageService {
|
||||
return GetReferenceDocumentResponse(
|
||||
content: try await macroExpansionManager.macroExpansion(for: data)
|
||||
)
|
||||
case .generatedInterface(let data):
|
||||
return GetReferenceDocumentResponse(
|
||||
content: try await generatedInterfaceManager.snapshot(of: data).text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
|
||||
}
|
||||
|
||||
func documentService(for uri: DocumentURI) -> LanguageService? {
|
||||
return documentService.value[uri.primaryFile ?? uri]
|
||||
return documentService.value[uri.buildSettingsFile]
|
||||
}
|
||||
|
||||
/// Set a language service for a document uri and returns if none exists already.
|
||||
|
||||
@@ -22,14 +22,13 @@ import XCTest
|
||||
final class SwiftInterfaceTests: XCTestCase {
|
||||
func testSystemModuleInterface() async throws {
|
||||
let testClient = try await TestSourceKitLSPClient()
|
||||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||||
let uri = DocumentURI(url)
|
||||
let uri = DocumentURI(for: .swift)
|
||||
|
||||
testClient.openDocument("import Foundation", uri: uri)
|
||||
|
||||
let resp = try await testClient.send(
|
||||
DefinitionRequest(
|
||||
textDocument: TextDocumentIdentifier(url),
|
||||
textDocument: TextDocumentIdentifier(uri),
|
||||
position: Position(line: 0, utf16index: 10)
|
||||
)
|
||||
)
|
||||
@@ -43,6 +42,30 @@ final class SwiftInterfaceTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testSystemModuleInterfaceReferenceDocument() async throws {
|
||||
let testClient = try await TestSourceKitLSPClient(
|
||||
capabilities: ClientCapabilities(experimental: [
|
||||
"workspace/getReferenceDocument": .bool(true)
|
||||
])
|
||||
)
|
||||
let uri = DocumentURI(for: .swift)
|
||||
|
||||
testClient.openDocument("import Foundation", uri: uri)
|
||||
|
||||
let response = try await testClient.send(
|
||||
DefinitionRequest(
|
||||
textDocument: TextDocumentIdentifier(uri),
|
||||
position: Position(line: 0, utf16index: 10)
|
||||
)
|
||||
)
|
||||
let location = try XCTUnwrap(response?.locations?.only)
|
||||
let referenceDocument = try await testClient.send(GetReferenceDocumentRequest(uri: location.uri))
|
||||
XCTAssert(
|
||||
referenceDocument.content.hasPrefix("import "),
|
||||
"Expected that the foundation swift interface starts with 'import ' but got '\(referenceDocument.content.prefix(100))'"
|
||||
)
|
||||
}
|
||||
|
||||
func testDefinitionInSystemModuleInterface() async throws {
|
||||
let project = try await IndexedSingleSwiftFileTestProject(
|
||||
"""
|
||||
@@ -86,6 +109,37 @@ final class SwiftInterfaceTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testDefinitionInSystemModuleInterfaceWithReferenceDocument() async throws {
|
||||
let project = try await IndexedSingleSwiftFileTestProject(
|
||||
"""
|
||||
public func libFunc() async {
|
||||
let a: 1️⃣String = "test"
|
||||
}
|
||||
""",
|
||||
capabilities: ClientCapabilities(experimental: [
|
||||
"workspace/getReferenceDocument": .bool(true)
|
||||
]),
|
||||
indexSystemModules: true
|
||||
)
|
||||
|
||||
let definition = try await project.testClient.send(
|
||||
DefinitionRequest(
|
||||
textDocument: TextDocumentIdentifier(project.fileURI),
|
||||
position: project.positions["1️⃣"]
|
||||
)
|
||||
)
|
||||
let location = try XCTUnwrap(definition?.locations?.only)
|
||||
let referenceDocument = try await project.testClient.send(GetReferenceDocumentRequest(uri: location.uri))
|
||||
let contents = referenceDocument.content
|
||||
let lineTable = LineTable(contents)
|
||||
let destinationLine = try XCTUnwrap(lineTable.line(at: location.range.lowerBound.line))
|
||||
.trimmingCharacters(in: .whitespaces)
|
||||
XCTAssert(
|
||||
destinationLine.hasPrefix("@frozen public struct String"),
|
||||
"Full line was: '\(destinationLine)'"
|
||||
)
|
||||
}
|
||||
|
||||
func testSwiftInterfaceAcrossModules() async throws {
|
||||
let project = try await SwiftPMTestProject(
|
||||
files: [
|
||||
@@ -135,6 +189,65 @@ final class SwiftInterfaceTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testSemanticFunctionalityInGeneratedInterface() async throws {
|
||||
let project = try await SwiftPMTestProject(
|
||||
files: [
|
||||
"MyLibrary/MyLibrary.swift": """
|
||||
public struct Lib {
|
||||
public func foo() -> String {}
|
||||
public init() {}
|
||||
}
|
||||
""",
|
||||
"Exec/main.swift": "import 1️⃣MyLibrary",
|
||||
],
|
||||
manifest: """
|
||||
let package = Package(
|
||||
name: "MyLibrary",
|
||||
targets: [
|
||||
.target(name: "MyLibrary"),
|
||||
.executableTarget(name: "Exec", dependencies: ["MyLibrary"])
|
||||
]
|
||||
)
|
||||
""",
|
||||
capabilities: ClientCapabilities(experimental: [
|
||||
"workspace/getReferenceDocument": .bool(true)
|
||||
]),
|
||||
enableBackgroundIndexing: true
|
||||
)
|
||||
|
||||
let (mainUri, mainPositions) = try project.openDocument("main.swift")
|
||||
let response =
|
||||
try await project.testClient.send(
|
||||
DefinitionRequest(
|
||||
textDocument: TextDocumentIdentifier(mainUri),
|
||||
position: mainPositions["1️⃣"]
|
||||
)
|
||||
)
|
||||
let referenceDocumentUri = try XCTUnwrap(response?.locations?.only).uri
|
||||
let referenceDocument = try await project.testClient.send(GetReferenceDocumentRequest(uri: referenceDocumentUri))
|
||||
let stringIndex = try XCTUnwrap(referenceDocument.content.firstRange(of: "-> String"))
|
||||
let (stringLine, stringColumn) = LineTable(referenceDocument.content)
|
||||
.lineAndUTF16ColumnOf(referenceDocument.content.index(stringIndex.lowerBound, offsetBy: 3))
|
||||
|
||||
project.testClient.send(
|
||||
DidOpenTextDocumentNotification(
|
||||
textDocument: TextDocumentItem(
|
||||
uri: referenceDocumentUri,
|
||||
language: .swift,
|
||||
version: 0,
|
||||
text: referenceDocument.content
|
||||
)
|
||||
)
|
||||
)
|
||||
let hover = try await project.testClient.send(
|
||||
HoverRequest(
|
||||
textDocument: TextDocumentIdentifier(referenceDocumentUri),
|
||||
position: Position(line: stringLine, utf16index: stringColumn)
|
||||
)
|
||||
)
|
||||
XCTAssertNotNil(hover)
|
||||
}
|
||||
|
||||
func testJumpToSynthesizedExtensionMethodInSystemModuleWithoutIndex() async throws {
|
||||
let testClient = try await TestSourceKitLSPClient()
|
||||
let uri = DocumentURI(for: .swift)
|
||||
|
||||
Reference in New Issue
Block a user