Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 84 additions & 33 deletions Sources/BuildServerIntegration/BuildServerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -486,18 +486,16 @@ package actor BuildServerManager: QueueBasedMessageHandler {

private let cachedSourceFilesAndDirectories = Cache<SourceFilesAndDirectoriesKey, SourceFilesAndDirectories>()

/// The latest map of copied file URIs to their original source locations.
/// Task that computes the latest map of copied file URIs to their original source locations.
private var copiedFileMap: Task<[DocumentURI: DocumentURI], Never>?

/// The last computed copied file map, which may be out-of-date.
///
/// We don't use a `Cache` for this because we can provide reasonable functionality even without or with an
/// out-of-date copied file map - in the worst case we jump to a file in the build directory instead of the source
/// directory.
/// We don't want to block requests like definition on receiving up-to-date index information from the build server.
/// Even with out-of-date information for the copied file map,we can provide reasonable functionality - in the worst
/// case we jump to a file in the build directory instead of the source directory. We don't want to block requests
/// like definition on receiving up-to-date build target information from the build server.
private var cachedCopiedFileMap: [DocumentURI: DocumentURI] = [:]

/// The latest task to update the `cachedCopiedFileMap`. This allows us to cancel previous tasks to update the copied
/// file map when a new update is requested.
private var copiedFileMapUpdateTask: Task<Void, Never>?

/// The `SourceKitInitializeBuildResponseData` received from the `build/initialize` request, if any.
package var initializationData: SourceKitInitializeBuildResponseData? {
get async {
Expand Down Expand Up @@ -1096,10 +1094,10 @@ package actor BuildServerManager: QueueBasedMessageHandler {
}

@discardableResult
package func scheduleRecomputeCopyFileMap() -> Task<Void, Never> {
let task = Task { [previousUpdateTask = copiedFileMapUpdateTask] in
package func scheduleRecomputeCopyFileMap() -> Task<[DocumentURI: DocumentURI], Never> {
let task = Task<[DocumentURI: DocumentURI], Never> { [previousUpdateTask = copiedFileMap] in
previousUpdateTask?.cancel()
await orLog("Re-computing copy file map") {
return await orLog("Re-computing copy file map") {
let sourceFilesAndDirectories = try await self.sourceFilesAndDirectories()
try Task.checkCancellation()
var copiedFileMap: [DocumentURI: DocumentURI] = [:]
Expand All @@ -1109,12 +1107,25 @@ package actor BuildServerManager: QueueBasedMessageHandler {
}
}
self.cachedCopiedFileMap = copiedFileMap
}
return copiedFileMap
} ?? [:]
}
copiedFileMapUpdateTask = task
copiedFileMap = task
return task
}

/// Whether the build server knows about the document with the given URI. This can be either because the file is part
/// of one of the targets or because the document is a copy destination of a source file.
package func canHandle(_ document: DocumentURI) async -> Bool {
if await !targets(for: document).isEmpty {
return true
}
if await self.copiedFileMap?.value[document] != nil {
return true
}
return false
}

/// Returns all the targets that the document is part of.
package func targets(for document: DocumentURI) async -> [BuildTargetIdentifier] {
guard let targets = await sourceFileInfo(for: document)?.targets else {
Expand Down Expand Up @@ -1240,7 +1251,32 @@ package actor BuildServerManager: QueueBasedMessageHandler {
)
}
return buildSettingsFromBuildServer
}

/// If the given document is the copy destination of a source file, return fallback build settings based on the
/// original file. This allows us to provide semantic functionality in the destinations of copied files.
private func fallbackBuildSettingsInferredFromCopySource(
of document: DocumentURI,
target explicitlyRequestedTarget: BuildTargetIdentifier?,
language: Language?,
fallbackAfterTimeout: Bool
) async throws -> FileBuildSettings? {
guard let copySource = cachedCopiedFileMap[document] else {
return nil
}
let copySourceSettings = await self.buildSettingsInferredFromMainFile(
for: copySource,
target: explicitlyRequestedTarget,
language: language,
fallbackAfterTimeout: fallbackAfterTimeout,
allowInferenceFromRelatedFile: false
)
guard var copySourceSettings, !copySourceSettings.isFallback else {
return nil
}
copySourceSettings.isFallback = true
copySourceSettings = copySourceSettings.patching(newFile: document, originalFile: copySource)
return copySourceSettings
}

/// Try finding a source file with the same language as `document` in the same directory as `document` and patch its
Expand All @@ -1258,22 +1294,25 @@ package actor BuildServerManager: QueueBasedMessageHandler {
guard let language = language ?? Language(inferredFromFileExtension: document) else {
return nil
}
let siblingFile = try await self.sourceFilesAndDirectories().files.compactMap { (uri, info) -> DocumentURI? in
guard info.isBuildable, uri.fileURL?.deletingLastPathComponent() == directory else {
return nil
}
if let explicitlyRequestedTarget, !info.targets.contains(explicitlyRequestedTarget) {
return nil
}
// Only consider build settings from sibling files that appear to have the same language. In theory, we might skip
// valid sibling files because of this since non-standard file extension might be mapped to `language` by the
// build server, but this is a good first check to avoid requesting build settings for too many documents. And
// since all of this is fallback-logic, skipping over possibly valid files is not a correctness issue.
guard let siblingLanguage = Language(inferredFromFileExtension: uri), siblingLanguage == language else {
return nil
}
return uri
}.sorted(by: { $0.pseudoPath < $1.pseudoPath }).first
var siblingFile: DocumentURI? = cachedCopiedFileMap[document]
if siblingFile == nil {
siblingFile = try await self.sourceFilesAndDirectories().files.compactMap { (uri, info) -> DocumentURI? in
guard info.isBuildable, uri.fileURL?.deletingLastPathComponent() == directory else {
return nil
}
if let explicitlyRequestedTarget, !info.targets.contains(explicitlyRequestedTarget) {
return nil
}
// Only consider build settings from sibling files that appear to have the same language. In theory, we might skip
// valid sibling files because of this since non-standard file extension might be mapped to `language` by the
// build server, but this is a good first check to avoid requesting build settings for too many documents. And
// since all of this is fallback-logic, skipping over possibly valid files is not a correctness issue.
guard let siblingLanguage = Language(inferredFromFileExtension: uri), siblingLanguage == language else {
return nil
}
return uri
}.sorted(by: { $0.pseudoPath < $1.pseudoPath }).first
}

guard let siblingFile else {
return nil
Expand All @@ -1284,7 +1323,7 @@ package actor BuildServerManager: QueueBasedMessageHandler {
target: explicitlyRequestedTarget,
language: language,
fallbackAfterTimeout: fallbackAfterTimeout,
allowInferenceFromSiblingFile: false
allowInferenceFromRelatedFile: false
)
guard var siblingSettings, !siblingSettings.isFallback else {
return nil
Expand Down Expand Up @@ -1318,7 +1357,7 @@ package actor BuildServerManager: QueueBasedMessageHandler {
target explicitlyRequestedTarget: BuildTargetIdentifier? = nil,
language: Language?,
fallbackAfterTimeout: Bool,
allowInferenceFromSiblingFile: Bool = true
allowInferenceFromRelatedFile: Bool = true
) async -> FileBuildSettings? {
if buildServerAdapter == nil {
guard let language = language ?? Language(inferredFromFileExtension: document) else {
Expand Down Expand Up @@ -1377,7 +1416,19 @@ package actor BuildServerManager: QueueBasedMessageHandler {
fallbackAfterTimeout: fallbackAfterTimeout
)
case .result(nil):
if allowInferenceFromSiblingFile {
if allowInferenceFromRelatedFile {
let settingsFromCopySource = await orLog("Inferring build settings from copy source") {
try await self.fallbackBuildSettingsInferredFromCopySource(
of: document,
target: explicitlyRequestedTarget,
language: language,
fallbackAfterTimeout: fallbackAfterTimeout
)
}
if let settingsFromCopySource {
return settingsFromCopySource
}

let settingsFromSibling = await orLog("Inferring build settings from sibling file") {
try await self.fallbackBuildSettingsInferredFromSiblingFile(
of: document,
Expand Down
2 changes: 1 addition & 1 deletion Sources/SourceKitLSP/SourceKitLSPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ package actor SourceKitLSPServer {
// Pick the workspace with the best FileHandlingCapability for this file.
// If there is a tie, use the workspace that occurred first in the list.
var bestWorkspace = await self.workspaces.asyncFirst {
await !$0.buildServerManager.targets(for: uri).isEmpty
await $0.buildServerManager.canHandle(uri)
}
if bestWorkspace == nil {
// We weren't able to handle the document with any of the known workspaces. See if any of the document's parent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import SKTestSupport
import SwiftExtensions
import XCTest

class CopiedHeaderTests: SourceKitLSPTestCase {
class CopyDestinationTests: SourceKitLSPTestCase {
actor BuildServer: CustomBuildServer {
let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker()
private let projectRoot: URL

private var headerCopyDestination: URL {
var headerCopyDestination: URL {
projectRoot.appending(components: "header-copy", "CopiedTest.h")
}

Expand Down Expand Up @@ -51,8 +51,6 @@ class CopiedHeaderTests: SourceKitLSPTestCase {
data: SourceKitSourceItemData(
language: .c,
kind: .source,
outputPath: nil,
copyDestinations: nil
).encodeToLSPAny()
),
SourceItem(
Expand All @@ -63,7 +61,6 @@ class CopiedHeaderTests: SourceKitLSPTestCase {
data: SourceKitSourceItemData(
language: .c,
kind: .header,
outputPath: nil,
copyDestinations: [DocumentURI(headerCopyDestination)]
).encodeToLSPAny()
),
Expand All @@ -76,7 +73,9 @@ class CopiedHeaderTests: SourceKitLSPTestCase {
_ request: TextDocumentSourceKitOptionsRequest
) throws -> TextDocumentSourceKitOptionsResponse? {
return TextDocumentSourceKitOptionsResponse(compilerArguments: [
request.textDocument.uri.pseudoPath, "-I", try headerCopyDestination.deletingLastPathComponent().filePath,
request.textDocument.uri.pseudoPath,
"-I", try headerCopyDestination.deletingLastPathComponent().filePath,
"-D", "FOO",
])
}

Expand All @@ -93,6 +92,32 @@ class CopiedHeaderTests: SourceKitLSPTestCase {
}
}

func testJumpToCopiedHeader() async throws {
let project = try await CustomBuildServerTestProject(
files: [
"Test.h": """
void hello();
""",
"Test.c": """
#include <CopiedTest.h>

void test() {
1️⃣hello();
}
""",
],
buildServer: BuildServer.self,
enableBackgroundIndexing: true,
)
try await project.testClient.send(SynchronizeRequest(copyFileMap: true))

let (uri, positions) = try project.openDocument("Test.c")
let response = try await project.testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
XCTAssertEqual(response?.locations?.map(\.uri), [try project.uri(for: "Test.h")])
}

func testFindReferencesInCopiedHeader() async throws {
let project = try await CustomBuildServerTestProject(
files: [
Expand Down Expand Up @@ -193,4 +218,29 @@ class CopiedHeaderTests: SourceKitLSPTestCase {
}
XCTAssertEqual(info.location, try project.location(from: "1️⃣", to: "1️⃣", in: "Test.h"))
}

func testSemanticFunctionalityInCopiedHeader() async throws {
let contents = """
#ifdef FOO
typedef void 1️⃣MY_VOID2️⃣;
#else
typedef void MY_VOID;
#endif
3️⃣MY_VOID hello();
"""

let project = try await CustomBuildServerTestProject(
files: ["Test.h": contents],
buildServer: BuildServer.self,
enableBackgroundIndexing: false,
)
try await project.testClient.send(SynchronizeRequest(copyFileMap: true))
let headerUri = try await DocumentURI(project.buildServer().headerCopyDestination)

let positions = project.testClient.openDocument(contents, uri: headerUri, language: .c)
let response = try await project.testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(headerUri), position: positions["3️⃣"])
)
XCTAssertEqual(response?.locations, [try project.location(from: "1️⃣", to: "2️⃣", in: "Test.h")])
}
}
Loading