Skip to content

Commit 8d73731

Browse files
committed
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
1 parent ec461d6 commit 8d73731

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
}
@@ -1590,14 +1590,14 @@ extension SourceKitLSPServer {
15901590
}
15911591

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

1595-
guard let workspace = await workspaceForDocument(uri: primaryFileURI) else {
1596-
throw ResponseError.workspaceNotOpen(primaryFileURI)
1595+
guard let workspace = await workspaceForDocument(uri: buildSettingsUri) else {
1596+
throw ResponseError.workspaceNotOpen(buildSettingsUri)
15971597
}
15981598

1599-
guard let languageService = workspace.documentService(for: primaryFileURI) else {
1600-
throw ResponseError.unknown("No Language Service for URI: \(primaryFileURI)")
1599+
guard let languageService = workspace.documentService(for: buildSettingsUri) else {
1600+
throw ResponseError.unknown("No Language Service for URI: \(buildSettingsUri)")
16011601
}
16021602

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