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:
Alex Hoppen
2025-01-03 14:35:01 +01:00
committed by GitHub
13 changed files with 592 additions and 159 deletions

View File

@@ -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))
],

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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)
}
}

View 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)
}
}
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
)
}
}

View File

@@ -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 {

View File

@@ -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
)
}
}
}

View File

@@ -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.

View File

@@ -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)