Skip to content

Commit f653ef3

Browse files
authored
Merge pull request swiftlang#1887 from ahoppen/generated-interface-reference-document
Support semantic functionality in generated interfaces if the client supports `getReferenceDocument`
2 parents 233f2e6 + 8d73731 commit f653ef3

13 files changed

+589
-156
lines changed

Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ package struct IndexedSingleSwiftFileTestProject {
5151
/// - cleanUp: Whether to remove the temporary directory when the SourceKit-LSP server shuts down.
5252
package init(
5353
_ markedText: String,
54+
capabilities: ClientCapabilities = ClientCapabilities(),
5455
indexSystemModules: Bool = false,
5556
allowBuildFailure: Bool = false,
5657
workspaceDirectory: URL? = nil,
@@ -153,6 +154,7 @@ package struct IndexedSingleSwiftFileTestProject {
153154
)
154155
self.testClient = try await TestSourceKitLSPClient(
155156
options: options,
157+
capabilities: capabilities,
156158
workspaceFolders: [
157159
WorkspaceFolder(uri: DocumentURI(testWorkspaceDirectory))
158160
],

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ target_sources(SourceKitLSP PRIVATE
4848
Swift/DocumentSymbols.swift
4949
Swift/ExpandMacroCommand.swift
5050
Swift/FoldingRange.swift
51+
Swift/GeneratedInterfaceDocumentURLData.swift
52+
Swift/GeneratedInterfaceManager.swift
53+
Swift/GeneratedInterfaceManager.swift
5154
Swift/MacroExpansion.swift
5255
Swift/MacroExpansionReferenceDocumentURLData.swift
5356
Swift/OpenInterface.swift
54-
Swift/RefactoringResponse.swift
5557
Swift/RefactoringEdit.swift
58+
Swift/RefactoringResponse.swift
5659
Swift/ReferenceDocumentURL.swift
5760
Swift/RelatedIdentifiers.swift
5861
Swift/RewriteSourceKitPlaceholders.swift

Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc
7474
case (.documentUpdate(let selfUri), .documentUpdate(let otherUri)):
7575
return selfUri == otherUri
7676
case (.documentUpdate(let selfUri), .documentRequest(let otherUri)):
77-
return selfUri == otherUri
77+
return selfUri.buildSettingsFile == otherUri.buildSettingsFile
7878
case (.documentRequest(let selfUri), .documentUpdate(let otherUri)):
79-
return selfUri == otherUri
79+
return selfUri.buildSettingsFile == otherUri.buildSettingsFile
8080

8181
// documentRequest
8282
case (.documentRequest, .documentRequest):

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ package actor SourceKitLSPServer {
242242
}
243243

244244
package func workspaceForDocument(uri: DocumentURI) async -> Workspace? {
245-
let uri = uri.primaryFile ?? uri
245+
let uri = uri.buildSettingsFile
246246
if let cachedWorkspace = self.workspaceForUri[uri]?.value {
247247
return cachedWorkspace
248248
}
@@ -1592,14 +1592,14 @@ extension SourceKitLSPServer {
15921592
}
15931593

15941594
func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse {
1595-
let primaryFileURI = try ReferenceDocumentURL(from: req.uri).primaryFile
1595+
let buildSettingsUri = try ReferenceDocumentURL(from: req.uri).buildSettingsFile
15961596

1597-
guard let workspace = await workspaceForDocument(uri: primaryFileURI) else {
1598-
throw ResponseError.workspaceNotOpen(primaryFileURI)
1597+
guard let workspace = await workspaceForDocument(uri: buildSettingsUri) else {
1598+
throw ResponseError.workspaceNotOpen(buildSettingsUri)
15991599
}
16001600

1601-
guard let languageService = workspace.documentService(for: primaryFileURI) else {
1602-
throw ResponseError.unknown("No Language Service for URI: \(primaryFileURI)")
1601+
guard let languageService = workspace.documentService(for: buildSettingsUri) else {
1602+
throw ResponseError.unknown("No Language Service for URI: \(buildSettingsUri)")
16031603
}
16041604

16051605
return try await languageService.getReferenceDocument(req)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import LanguageServerProtocol
15+
16+
/// Represents url of generated interface reference document.
17+
18+
package struct GeneratedInterfaceDocumentURLData: Hashable, ReferenceURLData {
19+
package static let documentType = "generated-swift-interface"
20+
21+
private struct Parameters {
22+
static let moduleName = "moduleName"
23+
static let groupName = "groupName"
24+
static let sourcekitdDocumentName = "sourcekitdDocument"
25+
static let buildSettingsFrom = "buildSettingsFrom"
26+
}
27+
28+
/// The module that should be shown in this generated interface.
29+
let moduleName: String
30+
31+
/// The group that should be shown in this generated interface, if applicable.
32+
let groupName: String?
33+
34+
/// The name by which this document is referred to in sourcekitd.
35+
let sourcekitdDocumentName: String
36+
37+
/// The document from which the build settings for the generated interface should be inferred.
38+
let buildSettingsFrom: DocumentURI
39+
40+
var displayName: String {
41+
if let groupName {
42+
return "\(moduleName).\(groupName.replacing("/", with: ".")).swiftinterface"
43+
}
44+
return "\(moduleName).swiftinterface"
45+
}
46+
47+
var queryItems: [URLQueryItem] {
48+
var result = [
49+
URLQueryItem(name: Parameters.moduleName, value: moduleName)
50+
]
51+
if let groupName {
52+
result.append(URLQueryItem(name: Parameters.groupName, value: groupName))
53+
}
54+
result += [
55+
URLQueryItem(name: Parameters.sourcekitdDocumentName, value: sourcekitdDocumentName),
56+
URLQueryItem(name: Parameters.buildSettingsFrom, value: buildSettingsFrom.stringValue),
57+
]
58+
return result
59+
}
60+
61+
var uri: DocumentURI {
62+
get throws {
63+
try ReferenceDocumentURL.generatedInterface(self).uri
64+
}
65+
}
66+
67+
init(moduleName: String, groupName: String?, sourcekitdDocumentName: String, primaryFile: DocumentURI) {
68+
self.moduleName = moduleName
69+
self.groupName = groupName
70+
self.sourcekitdDocumentName = sourcekitdDocumentName
71+
self.buildSettingsFrom = primaryFile
72+
}
73+
74+
init(queryItems: [URLQueryItem]) throws {
75+
guard let moduleName = queryItems.last(where: { $0.name == Parameters.moduleName })?.value,
76+
let sourcekitdDocumentName = queryItems.last(where: { $0.name == Parameters.sourcekitdDocumentName })?.value,
77+
let primaryFile = queryItems.last(where: { $0.name == Parameters.buildSettingsFrom })?.value
78+
else {
79+
throw ReferenceDocumentURLError(description: "Invalid queryItems for generated interface reference document url")
80+
}
81+
82+
self.moduleName = moduleName
83+
self.groupName = queryItems.last(where: { $0.name == Parameters.groupName })?.value
84+
self.sourcekitdDocumentName = sourcekitdDocumentName
85+
self.buildSettingsFrom = try DocumentURI(string: primaryFile)
86+
}
87+
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import SKLogging
15+
import SKUtilities
16+
import SourceKitD
17+
import SwiftExtensions
18+
19+
/// When information about a generated interface is requested, this opens the generated interface in sourcekitd and
20+
/// caches the generated interface contents.
21+
///
22+
/// It keeps the generated interface open in sourcekitd until the corresponding reference document is closed in the
23+
/// editor. Additionally, it also keeps a few recently requested interfaces cached. This way we don't need to recompute
24+
/// the generated interface contents between the initial generated interface request to find a USR's position in the
25+
/// interface until the editor actually opens the reference document.
26+
actor GeneratedInterfaceManager {
27+
private struct OpenGeneratedInterfaceDocumentDetails {
28+
let url: GeneratedInterfaceDocumentURLData
29+
30+
/// The contents of the generated interface.
31+
let snapshot: DocumentSnapshot
32+
33+
/// The number of `GeneratedInterfaceManager` that are actively working with the sourcekitd document. If this value
34+
/// is 0, the generated interface may be closed in sourcekitd.
35+
///
36+
/// Usually, this value is 1, while the reference document for this generated interface is open in the editor.
37+
var refCount: Int
38+
}
39+
40+
private weak var swiftLanguageService: SwiftLanguageService?
41+
42+
/// The number of generated interface documents that are not in editor but should still be cached.
43+
private let cacheSize = 2
44+
45+
/// Details about the generated interfaces that are currently open in sourcekitd.
46+
///
47+
/// Conceptually, this is a dictionary with `url` being the key. To prevent excessive memory usage we only keep
48+
/// `cacheSize` entries with a ref count of 0 in the array. Older entries are at the end of the list, newer entries
49+
/// at the front.
50+
private var openInterfaces: [OpenGeneratedInterfaceDocumentDetails] = []
51+
52+
init(swiftLanguageService: SwiftLanguageService) {
53+
self.swiftLanguageService = swiftLanguageService
54+
}
55+
56+
/// If there are more than `cacheSize` entries in `openInterfaces` that have a ref count of 0, close the oldest ones.
57+
private func purgeCache() {
58+
var documentsToClose: [String] = []
59+
while openInterfaces.count(where: { $0.refCount == 0 }) > cacheSize,
60+
let indexToPurge = openInterfaces.lastIndex(where: { $0.refCount == 0 })
61+
{
62+
documentsToClose.append(openInterfaces[indexToPurge].url.sourcekitdDocumentName)
63+
openInterfaces.remove(at: indexToPurge)
64+
}
65+
if !documentsToClose.isEmpty, let swiftLanguageService {
66+
Task {
67+
let sourcekitd = swiftLanguageService.sourcekitd
68+
for documentToClose in documentsToClose {
69+
await orLog("Closing generated interface") {
70+
_ = try await swiftLanguageService.sendSourcekitdRequest(
71+
sourcekitd.dictionary([
72+
sourcekitd.keys.request: sourcekitd.requests.editorClose,
73+
sourcekitd.keys.name: documentToClose,
74+
sourcekitd.keys.cancelBuilds: 0,
75+
]),
76+
fileContents: nil
77+
)
78+
}
79+
}
80+
}
81+
}
82+
}
83+
84+
/// If we don't have the generated interface for the given `document` open in sourcekitd, open it, otherwise return
85+
/// its details from the cache.
86+
///
87+
/// If `incrementingRefCount` is `true`, then the document manager will keep the generated interface open in
88+
/// sourcekitd, independent of the cache size. If `incrementingRefCount` is `true`, then `decrementRefCount` must be
89+
/// called to allow the document to be closed again.
90+
private func details(
91+
for document: GeneratedInterfaceDocumentURLData,
92+
incrementingRefCount: Bool
93+
) async throws -> OpenGeneratedInterfaceDocumentDetails {
94+
func loadFromCache() -> OpenGeneratedInterfaceDocumentDetails? {
95+
guard let cachedIndex = openInterfaces.firstIndex(where: { $0.url == document }) else {
96+
return nil
97+
}
98+
if incrementingRefCount {
99+
openInterfaces[cachedIndex].refCount += 1
100+
}
101+
return openInterfaces[cachedIndex]
102+
103+
}
104+
if let cached = loadFromCache() {
105+
return cached
106+
}
107+
108+
guard let swiftLanguageService else {
109+
// `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do.
110+
throw ResponseError.unknown("Connection to the editor closed")
111+
}
112+
113+
let sourcekitd = swiftLanguageService.sourcekitd
114+
115+
let keys = sourcekitd.keys
116+
let skreq = sourcekitd.dictionary([
117+
keys.request: sourcekitd.requests.editorOpenInterface,
118+
keys.moduleName: document.moduleName,
119+
keys.groupName: document.groupName,
120+
keys.name: document.sourcekitdDocumentName,
121+
keys.synthesizedExtension: 1,
122+
keys.compilerArgs: await swiftLanguageService.buildSettings(for: try document.uri, fallbackAfterTimeout: false)?
123+
.compilerArgs as [SKDRequestValue]?,
124+
])
125+
126+
let dict = try await swiftLanguageService.sendSourcekitdRequest(skreq, fileContents: nil)
127+
128+
guard let contents: String = dict[keys.sourceText] else {
129+
throw ResponseError.unknown("sourcekitd response is missing sourceText")
130+
}
131+
132+
if let cached = loadFromCache() {
133+
// Another request raced us to create the generated interface. Discard what we computed here and return the cached
134+
// value.
135+
await orLog("Closing generated interface created during race") {
136+
_ = try await swiftLanguageService.sendSourcekitdRequest(
137+
sourcekitd.dictionary([
138+
keys.request: sourcekitd.requests.editorClose,
139+
keys.name: document.sourcekitdDocumentName,
140+
keys.cancelBuilds: 0,
141+
]),
142+
fileContents: nil
143+
)
144+
}
145+
return cached
146+
}
147+
148+
let details = OpenGeneratedInterfaceDocumentDetails(
149+
url: document,
150+
snapshot: DocumentSnapshot(
151+
uri: try document.uri,
152+
language: .swift,
153+
version: 0,
154+
lineTable: LineTable(contents)
155+
),
156+
refCount: incrementingRefCount ? 1 : 0
157+
)
158+
openInterfaces.insert(details, at: 0)
159+
purgeCache()
160+
return details
161+
}
162+
163+
private func decrementRefCount(for document: GeneratedInterfaceDocumentURLData) {
164+
guard let cachedIndex = openInterfaces.firstIndex(where: { $0.url == document }) else {
165+
logger.fault(
166+
"Generated interface document for \(document.moduleName) is not open anymore. Unbalanced retain and releases?"
167+
)
168+
return
169+
}
170+
if openInterfaces[cachedIndex].refCount == 0 {
171+
logger.fault(
172+
"Generated interface document for \(document.moduleName) is already 0. Unbalanced retain and releases?"
173+
)
174+
return
175+
}
176+
openInterfaces[cachedIndex].refCount -= 1
177+
purgeCache()
178+
}
179+
180+
func position(ofUsr usr: String, in document: GeneratedInterfaceDocumentURLData) async throws -> Position {
181+
guard let swiftLanguageService else {
182+
// `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do.
183+
throw ResponseError.unknown("Connection to the editor closed")
184+
}
185+
186+
let details = try await details(for: document, incrementingRefCount: true)
187+
defer {
188+
decrementRefCount(for: document)
189+
}
190+
191+
let sourcekitd = swiftLanguageService.sourcekitd
192+
let keys = sourcekitd.keys
193+
let skreq = sourcekitd.dictionary([
194+
keys.request: sourcekitd.requests.editorFindUSR,
195+
keys.sourceFile: document.sourcekitdDocumentName,
196+
keys.usr: usr,
197+
])
198+
199+
let dict = try await swiftLanguageService.sendSourcekitdRequest(skreq, fileContents: details.snapshot.text)
200+
guard let offset: Int = dict[keys.offset] else {
201+
throw ResponseError.unknown("Missing key 'offset'")
202+
}
203+
return details.snapshot.positionOf(utf8Offset: offset)
204+
}
205+
206+
func snapshot(of document: GeneratedInterfaceDocumentURLData) async throws -> DocumentSnapshot {
207+
return try await details(for: document, incrementingRefCount: false).snapshot
208+
}
209+
210+
func open(document: GeneratedInterfaceDocumentURLData) async throws {
211+
_ = try await details(for: document, incrementingRefCount: true)
212+
}
213+
214+
func close(document: GeneratedInterfaceDocumentURLData) async {
215+
decrementRefCount(for: document)
216+
}
217+
218+
func reopen(interfacesWithBuildSettingsFrom buildSettingsFile: DocumentURI) async {
219+
for openInterface in openInterfaces {
220+
guard openInterface.url.buildSettingsFrom == buildSettingsFile else {
221+
continue
222+
}
223+
await orLog("Reopening generated interface") {
224+
// `MessageHandlingDependencyTracker` ensures that we don't handle a request for the generated interface while
225+
// it is being re-opened because `documentUpdate` and `documentRequest` use the `buildSettingsFile` to determine
226+
// their dependencies.
227+
await close(document: openInterface.url)
228+
openInterfaces.removeAll(where: { $0.url == openInterface.url })
229+
try await open(document: openInterface.url)
230+
}
231+
}
232+
}
233+
}

0 commit comments

Comments
 (0)