diff --git a/Sources/BuildServerIntegration/BuildServerManager.swift b/Sources/BuildServerIntegration/BuildServerManager.swift index 44e133b1c..35784efa4 100644 --- a/Sources/BuildServerIntegration/BuildServerManager.swift +++ b/Sources/BuildServerIntegration/BuildServerManager.swift @@ -962,6 +962,118 @@ package actor BuildServerManager: QueueBasedMessageHandler { return locations.map { locationAdjustedForCopiedFiles($0) } } + private func uriAdjustedForCopiedFiles(_ uri: DocumentURI) -> DocumentURI { + guard let originalUri = cachedCopiedFileMap[uri] else { + return uri + } + return originalUri + } + + package func workspaceEditAdjustedForCopiedFiles(_ workspaceEdit: WorkspaceEdit?) -> WorkspaceEdit? { + guard var edit = workspaceEdit else { + return nil + } + if let changes = edit.changes { + var newChanges: [DocumentURI: [TextEdit]] = [:] + for (uri, edits) in changes { + let newUri = self.uriAdjustedForCopiedFiles(uri) + newChanges[newUri, default: []] += edits + } + edit.changes = newChanges + } + if let documentChanges = edit.documentChanges { + edit.documentChanges = documentChanges.map { change in + switch change { + case .textDocumentEdit(var textEdit): + textEdit.textDocument.uri = self.uriAdjustedForCopiedFiles(textEdit.textDocument.uri) + return .textDocumentEdit(textEdit) + case .createFile(var create): + create.uri = self.uriAdjustedForCopiedFiles(create.uri) + return .createFile(create) + case .renameFile(var rename): + rename.oldUri = self.uriAdjustedForCopiedFiles(rename.oldUri) + rename.newUri = self.uriAdjustedForCopiedFiles(rename.newUri) + return .renameFile(rename) + case .deleteFile(var delete): + delete.uri = self.uriAdjustedForCopiedFiles(delete.uri) + return .deleteFile(delete) + } + } + } + return edit + } + + package func locationsOrLocationLinksAdjustedForCopiedFiles( + _ response: LocationsOrLocationLinksResponse? + ) -> LocationsOrLocationLinksResponse? { + guard let response = response else { + return nil + } + switch response { + case .locations(let locations): + let remappedLocations = self.locationsAdjustedForCopiedFiles(locations) + return .locations(remappedLocations) + case .locationLinks(let locationLinks): + let remappedLinks = locationLinks.map { link -> LocationLink in + let adjustedTargetLocation = self.locationAdjustedForCopiedFiles( + Location(uri: link.targetUri, range: link.targetRange) + ) + let adjustedTargetSelectionLocation = self.locationAdjustedForCopiedFiles( + Location(uri: link.targetUri, range: link.targetSelectionRange) + ) + return LocationLink( + originSelectionRange: link.originSelectionRange, + targetUri: adjustedTargetLocation.uri, + targetRange: adjustedTargetLocation.range, + targetSelectionRange: adjustedTargetSelectionLocation.range + ) + } + return .locationLinks(remappedLinks) + } + } + + package func typeHierarchyItemAdjustedForCopiedFiles(_ item: TypeHierarchyItem) -> TypeHierarchyItem { + let adjustedLocation = self.locationAdjustedForCopiedFiles(Location(uri: item.uri, range: item.range)) + let adjustedSelectionLocation = self.locationAdjustedForCopiedFiles( + Location(uri: item.uri, range: item.selectionRange) + ) + return TypeHierarchyItem( + name: item.name, + kind: item.kind, + tags: item.tags, + detail: item.detail, + uri: adjustedLocation.uri, + range: adjustedLocation.range, + selectionRange: adjustedSelectionLocation.range, + data: item.data + ) + } + + package func callHierarchyItemAdjustedForCopiedFiles(_ item: CallHierarchyItem) -> CallHierarchyItem { + let adjustedLocation = self.locationAdjustedForCopiedFiles(Location(uri: item.uri, range: item.range)) + let adjustedSelectionLocation = self.locationAdjustedForCopiedFiles( + Location(uri: item.uri, range: item.selectionRange) + ) + return CallHierarchyItem( + name: item.name, + kind: item.kind, + tags: item.tags, + detail: item.detail, + uri: adjustedLocation.uri, + range: adjustedLocation.range, + selectionRange: adjustedSelectionLocation.range, + data: .dictionary([ + "usr": item.data.flatMap { data in + if case let .dictionary(dict) = data { + return dict["usr"] + } + return nil + } ?? .null, + "uri": .string(adjustedLocation.uri.stringValue), + ]) + ) + } + @discardableResult package func scheduleRecomputeCopyFileMap() -> Task { let task = Task { [previousUpdateTask = copiedFileMapUpdateTask] in diff --git a/Sources/ClangLanguageService/ClangLanguageService.swift b/Sources/ClangLanguageService/ClangLanguageService.swift index 8f414222b..3c28ddfdc 100644 --- a/Sources/ClangLanguageService/ClangLanguageService.swift +++ b/Sources/ClangLanguageService/ClangLanguageService.swift @@ -481,7 +481,11 @@ extension ClangLanguageService { } package func declaration(_ req: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? { - return try await forwardRequestToClangd(req) + let result = try await forwardRequestToClangd(req) + guard let workspace = self.workspace.value else { + return result + } + return await workspace.buildServerManager.locationsOrLocationLinksAdjustedForCopiedFiles(result) } package func completion(_ req: CompletionRequest) async throws -> CompletionList { @@ -618,7 +622,11 @@ extension ClangLanguageService { } package func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit? { - return try await forwardRequestToClangd(request) + let workspaceEdit = try await forwardRequestToClangd(request) + guard let workspace = self.workspace.value else { + return workspaceEdit + } + return await workspace.buildServerManager.workspaceEditAdjustedForCopiedFiles(workspaceEdit) } // MARK: - Other @@ -634,7 +642,12 @@ extension ClangLanguageService { position: renameRequest.position ) let symbolDetail = try await forwardRequestToClangd(symbolInfoRequest).only - return (try await edits ?? WorkspaceEdit(), symbolDetail?.usr) + let workspaceEdit = try await edits ?? WorkspaceEdit() + guard let workspace = self.workspace.value else { + return (workspaceEdit, symbolDetail?.usr) + } + let remappedEdit = await workspace.buildServerManager.workspaceEditAdjustedForCopiedFiles(workspaceEdit) + return (remappedEdit ?? WorkspaceEdit(), symbolDetail?.usr) } package func syntacticDocumentTests( diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 98aed8037..81b9ff08a 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -1666,12 +1666,11 @@ extension SourceKitLSPServer { guard req.query.count >= minWorkspaceSymbolPatternLength else { return [] } - var symbolsAndIndex: [(symbol: SymbolOccurrence, index: CheckedIndex)] = [] + var symbolsIndexAndWorkspaces: [(symbol: SymbolOccurrence, index: CheckedIndex, workspace: Workspace)] = [] for workspace in workspaces { guard let index = await workspace.index(checkedFor: .deletedFiles) else { continue } - var symbolOccurrences: [SymbolOccurrence] = [] index.forEachCanonicalSymbolOccurrence( containing: req.query, anchorStart: false, @@ -1685,26 +1684,22 @@ extension SourceKitLSPServer { guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else { return true } - symbolOccurrences.append(symbol) + symbolsIndexAndWorkspaces.append((symbol, index, workspace)) return true } try Task.checkCancellation() - symbolsAndIndex += symbolOccurrences.map { - return ($0, index) - } } - return symbolsAndIndex.sorted(by: { $0.symbol < $1.symbol }).map { symbolOccurrence, index in + + return await symbolsIndexAndWorkspaces.sorted(by: { $0.symbol < $1.symbol }).asyncMap { + (symbolOccurrence, index, workspace) in let symbolPosition = Position( line: symbolOccurrence.location.line - 1, // 1-based -> 0-based // Technically we would need to convert the UTF-8 column to a UTF-16 column. This would require reading the // file. In practice they almost always coincide, so we accept the incorrectness here to avoid the file read. utf16index: symbolOccurrence.location.utf8Column - 1 ) - - let symbolLocation = Location( - uri: symbolOccurrence.location.documentUri, - range: Range(symbolPosition) - ) + let symbolLocation = Location(uri: symbolOccurrence.location.documentUri, range: Range(symbolPosition)) + let location = await workspace.buildServerManager.locationAdjustedForCopiedFiles(symbolLocation) let containerNames = index.containerNames(of: symbolOccurrence) let containerName: String? @@ -1722,7 +1717,7 @@ extension SourceKitLSPServer { name: symbolOccurrence.symbol.name, kind: symbolOccurrence.symbol.kind.asLspSymbolKind(), deprecated: nil, - location: symbolLocation, + location: location, containerName: containerName ) ) @@ -2142,7 +2137,8 @@ extension SourceKitLSPServer { // returning it to the client. if indexBasedResponse.isEmpty { return await orLog("Fallback definition request", level: .info) { - return try await languageService.definition(req) + let result = try await languageService.definition(req) + return await workspace.buildServerManager.locationsOrLocationLinksAdjustedForCopiedFiles(result) } } let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(indexBasedResponse) @@ -2202,7 +2198,8 @@ extension SourceKitLSPServer { return occurrences.compactMap { indexToLSPLocation($0.location) } } - return .locations(locations.sorted()) + let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(locations) + return .locations(remappedLocations.sorted()) } func references( @@ -2228,7 +2225,8 @@ extension SourceKitLSPServer { } return index.occurrences(ofUSR: usr, roles: roles).compactMap { indexToLSPLocation($0.location) } } - return locations.unique.sorted() + let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(locations) + return remappedLocations.unique.sorted() } private func indexToLSPCallHierarchyItem( @@ -2273,13 +2271,24 @@ extension SourceKitLSPServer { // For call hierarchy preparation we only locate the definition let usrs = symbols.compactMap(\.usr) + // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed + func indexToLSPCallHierarchyItem2( + definition: SymbolOccurrence, + index: CheckedIndex + ) -> CallHierarchyItem? { + return self.indexToLSPCallHierarchyItem(definition: definition, index: index) + } + // Only return a single call hierarchy item. Returning multiple doesn't make sense because they will all have the // same USR (because we query them by USR) and will thus expand to the exact same call hierarchy. - let callHierarchyItems = usrs.compactMap { (usr) -> CallHierarchyItem? in + let callHierarchyItems = await usrs.asyncCompactMap { (usr) -> CallHierarchyItem? in guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else { return nil } - return self.indexToLSPCallHierarchyItem(definition: definition, index: index) + guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else { + return nil + } + return await workspace.buildServerManager.callHierarchyItemAdjustedForCopiedFiles(item) }.sorted(by: { Location(uri: $0.uri, range: $0.range) < Location(uri: $1.uri, range: $1.range) }) // Ideally, we should show multiple symbols. But VS Code fails to display call hierarchies with multiple root items, @@ -2305,7 +2314,8 @@ extension SourceKitLSPServer { func incomingCalls(_ req: CallHierarchyIncomingCallsRequest) async throws -> [CallHierarchyIncomingCall]? { guard let data = extractCallHierarchyItemData(req.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index(checkedFor: .deletedFiles) + let workspace = await self.workspaceForDocument(uri: data.uri), + let index = await workspace.index(checkedFor: .deletedFiles) else { return [] } @@ -2348,28 +2358,32 @@ extension SourceKitLSPServer { return self.indexToLSPCallHierarchyItem(definition: definition, index: index) } - let calls = callersToCalls.compactMap { (caller: Symbol, calls: [SymbolOccurrence]) -> CallHierarchyIncomingCall? in + let calls = await callersToCalls.asyncCompactMap { (caller, callsList) -> CallHierarchyIncomingCall? in // Resolve the caller's definition to find its location guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: caller.usr) else { return nil } - let locations = calls.compactMap { indexToLSPLocation2($0.location) }.sorted() - guard !locations.isEmpty else { + let locations = callsList.compactMap { indexToLSPLocation2($0.location) }.sorted() + let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(locations) + guard !remappedLocations.isEmpty else { return nil } + guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else { return nil } + let remappedItem = await workspace.buildServerManager.callHierarchyItemAdjustedForCopiedFiles(item) - return CallHierarchyIncomingCall(from: item, fromRanges: locations.map(\.range)) + return CallHierarchyIncomingCall(from: remappedItem, fromRanges: remappedLocations.map(\.range)) } return calls.sorted(by: { $0.from.name < $1.from.name }) } func outgoingCalls(_ req: CallHierarchyOutgoingCallsRequest) async throws -> [CallHierarchyOutgoingCall]? { guard let data = extractCallHierarchyItemData(req.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index(checkedFor: .deletedFiles) + let workspace = await self.workspaceForDocument(uri: data.uri), + let index = await workspace.index(checkedFor: .deletedFiles) else { return [] } @@ -2390,13 +2404,14 @@ extension SourceKitLSPServer { let callableUsrs = [data.usr] + index.occurrences(relatedToUSR: data.usr, roles: .accessorOf).map(\.symbol.usr) let callOccurrences = callableUsrs.flatMap { index.occurrences(relatedToUSR: $0, roles: .containedBy) } .filter(\.shouldShowInCallHierarchy) - let calls = callOccurrences.compactMap { occurrence -> CallHierarchyOutgoingCall? in + let calls = await callOccurrences.asyncCompactMap { occurrence -> CallHierarchyOutgoingCall? in guard occurrence.symbol.kind.isCallable else { return nil } guard let location = indexToLSPLocation2(occurrence.location) else { return nil } + let remappedLocation = await workspace.buildServerManager.locationAdjustedForCopiedFiles(location) // Resolve the callee's definition to find its location guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) else { @@ -2406,8 +2421,9 @@ extension SourceKitLSPServer { guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else { return nil } + let remappedItem = await workspace.buildServerManager.callHierarchyItemAdjustedForCopiedFiles(item) - return CallHierarchyOutgoingCall(to: item, fromRanges: [location.range]) + return CallHierarchyOutgoingCall(to: remappedItem, fromRanges: [remappedLocation.range]) } return calls.sorted(by: { $0.to.name < $1.to.name }) } @@ -2493,7 +2509,25 @@ extension SourceKitLSPServer { } }.compactMap(\.usr) - let typeHierarchyItems = usrs.compactMap { (usr) -> TypeHierarchyItem? in + // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed + func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { + return self.indexToLSPLocation(location) + } + + // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed + func indexToLSPTypeHierarchyItem2( + definition: SymbolOccurrence, + moduleName: String?, + index: CheckedIndex + ) -> TypeHierarchyItem? { + return self.indexToLSPTypeHierarchyItem( + definition: definition, + moduleName: moduleName, + index: index + ) + } + + let typeHierarchyItems = await usrs.asyncCompactMap { (usr) -> TypeHierarchyItem? in guard let info = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else { return nil } @@ -2508,13 +2542,17 @@ extension SourceKitLSPServer { .using: break } - return self.indexToLSPTypeHierarchyItem( - definition: info, - moduleName: info.location.moduleName, - index: index - ) - } - .sorted(by: { $0.name < $1.name }) + + guard indexToLSPLocation2(info.location) != nil else { + return nil + } + + let moduleName = info.location.moduleName + guard let item = indexToLSPTypeHierarchyItem2(definition: info, moduleName: moduleName, index: index) else { + return nil + } + return await workspace.buildServerManager.typeHierarchyItemAdjustedForCopiedFiles(item) + }.sorted(by: { $0.name < $1.name }) if typeHierarchyItems.isEmpty { // When returning an empty array, VS Code fails with the following two errors. Returning `nil` works around those @@ -2546,7 +2584,8 @@ extension SourceKitLSPServer { func supertypes(_ req: TypeHierarchySupertypesRequest) async throws -> [TypeHierarchyItem]? { guard let data = extractTypeHierarchyItemData(req.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index(checkedFor: .deletedFiles) + let workspace = await self.workspaceForDocument(uri: data.uri), + let index = await workspace.index(checkedFor: .deletedFiles) else { return [] } @@ -2568,11 +2607,6 @@ extension SourceKitLSPServer { return index.occurrences(relatedToUSR: related.symbol.usr, roles: .baseOf) } - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) - } - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPTypeHierarchyItem2( definition: SymbolOccurrence, @@ -2588,24 +2622,25 @@ extension SourceKitLSPServer { // Convert occurrences to type hierarchy items let occurs = baseOccurs + retroactiveConformanceOccurs - let types = occurs.compactMap { occurrence -> TypeHierarchyItem? in + let types = await occurs.asyncCompactMap { occurrence -> TypeHierarchyItem? in // Resolve the supertype's definition to find its location guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) else { return nil } - return indexToLSPTypeHierarchyItem2( - definition: definition, - moduleName: definition.location.moduleName, - index: index - ) + let moduleName = definition.location.moduleName + guard let item = indexToLSPTypeHierarchyItem2(definition: definition, moduleName: moduleName, index: index) else { + return nil + } + return await workspace.buildServerManager.typeHierarchyItemAdjustedForCopiedFiles(item) } return types.sorted(by: { $0.name < $1.name }) } func subtypes(_ req: TypeHierarchySubtypesRequest) async throws -> [TypeHierarchyItem]? { guard let data = extractTypeHierarchyItemData(req.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index(checkedFor: .deletedFiles) + let workspace = await self.workspaceForDocument(uri: data.uri), + let index = await workspace.index(checkedFor: .deletedFiles) else { return [] } @@ -2613,11 +2648,6 @@ extension SourceKitLSPServer { // Resolve child types and extensions let occurs = index.occurrences(ofUSR: data.usr, roles: [.baseOf, .extendedBy]) - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) - } - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPTypeHierarchyItem2( definition: SymbolOccurrence, @@ -2632,7 +2662,7 @@ extension SourceKitLSPServer { } // Convert occurrences to type hierarchy items - let types = occurs.compactMap { occurrence -> TypeHierarchyItem? in + let types = await occurs.asyncCompactMap { occurrence -> TypeHierarchyItem? in if occurrence.relations.count > 1 { // An occurrence with a `baseOf` or `extendedBy` relation is an occurrence inside an inheritance clause. // Such an occurrence can only be the source of a single type, namely the one that the inheritance clause belongs @@ -2648,11 +2678,11 @@ extension SourceKitLSPServer { return nil } - return indexToLSPTypeHierarchyItem2( - definition: definition, - moduleName: definition.location.moduleName, - index: index - ) + let moduleName = definition.location.moduleName + guard let item = indexToLSPTypeHierarchyItem2(definition: definition, moduleName: moduleName, index: index) else { + return nil + } + return await workspace.buildServerManager.typeHierarchyItemAdjustedForCopiedFiles(item) } return types.sorted { $0.name < $1.name } } diff --git a/Tests/SourceKitLSPTests/CopiedHeaderTests.swift b/Tests/SourceKitLSPTests/CopiedHeaderTests.swift new file mode 100644 index 000000000..125ae6152 --- /dev/null +++ b/Tests/SourceKitLSPTests/CopiedHeaderTests.swift @@ -0,0 +1,196 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 BuildServerIntegration +@_spi(SourceKitLSP) import BuildServerProtocol +@_spi(SourceKitLSP) import LanguageServerProtocol +@_spi(Testing) @_spi(SourceKitLSP) import SKLogging +import SKTestSupport +import SwiftExtensions +import XCTest + +class CopiedHeaderTests: SourceKitLSPTestCase { + actor BuildServer: CustomBuildServer { + let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker() + private let projectRoot: URL + + private var headerCopyDestination: URL { + projectRoot.appending(components: "header-copy", "CopiedTest.h") + } + + init(projectRoot: URL, connectionToSourceKitLSP: any Connection) { + self.projectRoot = projectRoot + } + + func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { + return try initializationResponseSupportingBackgroundIndexing( + projectRoot: projectRoot, + outputPathsProvider: false + ) + } + + func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) -> BuildTargetSourcesResponse { + return BuildTargetSourcesResponse(items: [ + SourcesItem( + target: .dummy, + sources: [ + SourceItem( + uri: DocumentURI(projectRoot.appending(component: "Test.c")), + kind: .file, + generated: false, + dataKind: .sourceKit, + data: SourceKitSourceItemData( + language: .c, + kind: .source, + outputPath: nil, + copyDestinations: nil + ).encodeToLSPAny() + ), + SourceItem( + uri: DocumentURI(projectRoot.appending(component: "Test.h")), + kind: .file, + generated: false, + dataKind: .sourceKit, + data: SourceKitSourceItemData( + language: .c, + kind: .header, + outputPath: nil, + copyDestinations: [DocumentURI(headerCopyDestination)] + ).encodeToLSPAny() + ), + ] + ) + ]) + } + + func textDocumentSourceKitOptionsRequest( + _ request: TextDocumentSourceKitOptionsRequest + ) throws -> TextDocumentSourceKitOptionsResponse? { + return TextDocumentSourceKitOptionsResponse(compilerArguments: [ + request.textDocument.uri.pseudoPath, "-I", try headerCopyDestination.deletingLastPathComponent().filePath, + ]) + } + + func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse { + try FileManager.default.createDirectory( + at: headerCopyDestination.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + try FileManager.default.copyItem( + at: projectRoot.appending(component: "Test.h"), + to: headerCopyDestination + ) + return VoidResponse() + } + } + + func testFindReferencesInCopiedHeader() async throws { + let project = try await CustomBuildServerTestProject( + files: [ + "Test.h": """ + void 1️⃣hello(); + """, + "Test.c": """ + #include + + void test() { + 2️⃣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( + ReferencesRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["2️⃣"], + context: ReferencesContext(includeDeclaration: true) + ) + ) + let expected = [ + try project.location(from: "2️⃣", to: "2️⃣", in: "Test.c"), + try project.location(from: "1️⃣", to: "1️⃣", in: "Test.h"), + ] + XCTAssertEqual(response, expected) + } + + func testFindDeclarationInCopiedHeader() async throws { + let project = try await CustomBuildServerTestProject( + files: [ + "Test.h": """ + void 1️⃣hello2️⃣(); + """, + "Test.c": """ + #include + + void hello() {} + + void test() { + 3️⃣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( + DeclarationRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["3️⃣"] + ) + ) + XCTAssertEqual( + response?.locations, + [ + try project.location(from: "1️⃣", to: "2️⃣", in: "Test.h") + ] + ) + } + + func testWorkspaceSymbolsInCopiedHeader() async throws { + let project = try await CustomBuildServerTestProject( + files: [ + "Test.h": """ + void 1️⃣hello(); + """, + "Test.c": """ + #include + + void test() { + hello(); + } + """, + ], + buildServer: BuildServer.self, + enableBackgroundIndexing: true + ) + try await project.testClient.send(SynchronizeRequest(copyFileMap: true)) + + _ = try project.openDocument("Test.c") + let response = try await project.testClient.send( + WorkspaceSymbolsRequest(query: "hello") + ) + let item = try XCTUnwrap(response?.only) + guard case .symbolInformation(let info) = item else { + XCTFail("Expected a symbol information") + return + } + XCTAssertEqual(info.location, try project.location(from: "1️⃣", to: "1️⃣", in: "Test.h")) + } +}