-
Notifications
You must be signed in to change notification settings - Fork 283
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support semantic functionality in generated interfaces if the client …
…supports `getReferenceDocument` This allows us to provide semantic functionality inside the generated interfaces, such as hover or jump-to-definition. rdar://125663597
- Loading branch information
Showing
12 changed files
with
600 additions
and
155 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
Sources/SourceKitLSP/Swift/GeneratedInterfaceDocumentURLData.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 changes: 233 additions & 0 deletions
233
Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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-opned because `documentUpdate` and `documentRequest` use the `buildSettingsFle` to determine | ||
// their dependencies. | ||
await close(document: openInterface.url) | ||
openInterfaces.removeAll(where: { $0.url == openInterface.url }) | ||
try await open(document: openInterface.url) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.