Skip to content

Commit cd46834

Browse files
authored
Merge pull request #1892 from ahoppen/6.1/merge-main-2024-12-17
2 parents f57980a + 61c3d0c commit cd46834

15 files changed

+259
-69
lines changed

Sources/BuildSystemIntegration/BuildSystemManager.swift

+34-35
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,21 @@ package struct SourceFileInfo: Sendable {
5454
/// from non-test targets or files that don't actually contain any tests.
5555
package var mayContainTests: Bool
5656

57+
/// Source files returned here fall into two categories:
58+
/// - Buildable source files are files that can be built by the build system and that make sense to background index
59+
/// - Non-buildable source files include eg. the SwiftPM package manifest or header files. We have sufficient
60+
/// compiler arguments for these files to provide semantic editor functionality but we can't build them.
61+
package var isBuildable: Bool
62+
5763
fileprivate func merging(_ other: SourceFileInfo?) -> SourceFileInfo {
5864
guard let other else {
5965
return self
6066
}
6167
return SourceFileInfo(
6268
targets: targets.union(other.targets),
6369
isPartOfRootProject: other.isPartOfRootProject || isPartOfRootProject,
64-
mayContainTests: other.mayContainTests || mayContainTests
70+
mayContainTests: other.mayContainTests || mayContainTests,
71+
isBuildable: other.isBuildable || isBuildable
6572
)
6673
}
6774
}
@@ -327,11 +334,9 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
327334

328335
private var cachedTargetSources = RequestCache<BuildTargetSourcesRequest>()
329336

330-
/// The parameters with which `SourceFilesAndDirectories` can be cached in `cachedSourceFilesAndDirectories`.
331-
private struct SourceFilesAndDirectoriesKey: Hashable {
332-
let includeNonBuildableFiles: Bool
333-
let sourcesItems: [SourcesItem]
334-
}
337+
/// `SourceFilesAndDirectories` is a global property that only gets reset when the build targets change and thus
338+
/// has no real key.
339+
private struct SourceFilesAndDirectoriesKey: Hashable {}
335340

336341
private struct SourceFilesAndDirectories {
337342
/// The source files in the workspace, ie. all `SourceItem`s that have `kind == .file`.
@@ -678,7 +683,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
678683
package func targets(for document: DocumentURI) async -> Set<BuildTargetIdentifier> {
679684
return await orLog("Getting targets for source file") {
680685
var result: Set<BuildTargetIdentifier> = []
681-
let filesAndDirectories = try await sourceFilesAndDirectories(includeNonBuildableFiles: true)
686+
let filesAndDirectories = try await sourceFilesAndDirectories()
682687
if let targets = filesAndDirectories.files[document]?.targets {
683688
result.formUnion(targets)
684689
}
@@ -1033,50 +1038,44 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
10331038
return response.items
10341039
}
10351040

1036-
/// Returns all source files in the project that can be built.
1041+
/// Returns all source files in the project.
10371042
///
10381043
/// - SeeAlso: Comment in `sourceFilesAndDirectories` for a definition of what `buildable` means.
1039-
package func buildableSourceFiles() async throws -> [DocumentURI: SourceFileInfo] {
1040-
return try await sourceFilesAndDirectories(includeNonBuildableFiles: false).files
1044+
package func sourceFiles(includeNonBuildableFiles: Bool) async throws -> [DocumentURI: SourceFileInfo] {
1045+
let files = try await sourceFilesAndDirectories().files
1046+
if includeNonBuildableFiles {
1047+
return files
1048+
} else {
1049+
return files.filter(\.value.isBuildable)
1050+
}
10411051
}
10421052

10431053
/// Get all files and directories that are known to the build system, ie. that are returned by a `buildTarget/sources`
10441054
/// request for any target in the project.
10451055
///
1046-
/// Source files returned here fall into two categories:
1047-
/// - Buildable source files are files that can be built by the build system and that make sense to background index
1048-
/// - Non-buildable source files include eg. the SwiftPM package manifest or header files. We have sufficient
1049-
/// compiler arguments for these files to provide semantic editor functionality but we can't build them.
1050-
///
1051-
/// `includeNonBuildableFiles` determines whether non-buildable files should be included.
1052-
private func sourceFilesAndDirectories(includeNonBuildableFiles: Bool) async throws -> SourceFilesAndDirectories {
1053-
let targets = try await self.buildTargets()
1054-
let sourcesItems = try await self.sourceFiles(in: Set(targets.keys))
1055-
1056-
let key = SourceFilesAndDirectoriesKey(
1057-
includeNonBuildableFiles: includeNonBuildableFiles,
1058-
sourcesItems: sourcesItems
1059-
)
1056+
/// - Important: This method returns both buildable and non-buildable source files. Callers need to check
1057+
/// `SourceFileInfo.isBuildable` if they are only interested in buildable source files.
1058+
private func sourceFilesAndDirectories() async throws -> SourceFilesAndDirectories {
1059+
return try await cachedSourceFilesAndDirectories.get(
1060+
SourceFilesAndDirectoriesKey(),
1061+
isolation: self
1062+
) { key in
1063+
let targets = try await self.buildTargets()
1064+
let sourcesItems = try await self.sourceFiles(in: Set(targets.keys))
10601065

1061-
return try await cachedSourceFilesAndDirectories.get(key, isolation: self) { key in
10621066
var files: [DocumentURI: SourceFileInfo] = [:]
10631067
var directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)] = [:]
1064-
for sourcesItem in key.sourcesItems {
1068+
for sourcesItem in sourcesItems {
10651069
let target = targets[sourcesItem.target]?.target
10661070
let isPartOfRootProject = !(target?.tags.contains(.dependency) ?? false)
10671071
let mayContainTests = target?.tags.contains(.test) ?? true
1068-
if !key.includeNonBuildableFiles && (target?.tags.contains(.notBuildable) ?? false) {
1069-
continue
1070-
}
1071-
10721072
for sourceItem in sourcesItem.sources {
1073-
if !key.includeNonBuildableFiles && sourceItem.sourceKitData?.isHeader ?? false {
1074-
continue
1075-
}
10761073
let info = SourceFileInfo(
10771074
targets: [sourcesItem.target],
10781075
isPartOfRootProject: isPartOfRootProject,
1079-
mayContainTests: mayContainTests
1076+
mayContainTests: mayContainTests,
1077+
isBuildable: !(target?.tags.contains(.notBuildable) ?? false)
1078+
&& !(sourceItem.sourceKitData?.isHeader ?? false)
10801079
)
10811080
switch sourceItem.kind {
10821081
case .file:
@@ -1093,7 +1092,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
10931092
}
10941093

10951094
package func testFiles() async throws -> [DocumentURI] {
1096-
return try await buildableSourceFiles().compactMap { (uri, info) -> DocumentURI? in
1095+
return try await sourceFiles(includeNonBuildableFiles: false).compactMap { (uri, info) -> DocumentURI? in
10971096
guard info.isPartOfRootProject, info.mayContainTests else {
10981097
return nil
10991098
}

Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem {
9999
let args = command.commandLine
100100
for i in args.indices.reversed() {
101101
if args[i] == "-index-store-path" && i + 1 < args.count {
102-
return URL(fileURLWithPath: args[i + 1])
102+
return URL(
103+
fileURLWithPath: args[i + 1],
104+
relativeTo: URL(fileURLWithPath: command.directory, isDirectory: true)
105+
)
103106
}
104107
}
105108
}

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
587587
SourceItem(
588588
uri: DocumentURI($0),
589589
kind: $0.isDirectory ? .directory : .file,
590-
generated: false,
590+
generated: false
591591
)
592592
}
593593
result.append(SourcesItem(target: target, sources: sources))

Sources/LanguageServerProtocol/SupportTypes/DocumentURI.swift

+14-5
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,21 @@ public struct DocumentURI: Codable, Hashable, Sendable {
5656
/// fallback mode that drops semantic functionality.
5757
public var pseudoPath: String {
5858
if storage.isFileURL {
59-
return storage.withUnsafeFileSystemRepresentation { filePath in
60-
if let filePath {
61-
String(cString: filePath)
62-
} else {
63-
""
59+
return storage.withUnsafeFileSystemRepresentation { filePathPtr in
60+
guard let filePathPtr else {
61+
return ""
6462
}
63+
let filePath = String(cString: filePathPtr)
64+
#if os(Windows)
65+
// VS Code spells file paths with a lowercase drive letter, while the rest of Windows APIs use an uppercase
66+
// drive letter. Normalize the drive letter spelling to be uppercase.
67+
if filePath.first?.isASCII ?? false, filePath.first?.isLetter ?? false, filePath.first?.isLowercase ?? false,
68+
filePath.count > 1, filePath[filePath.index(filePath.startIndex, offsetBy: 1)] == ":"
69+
{
70+
return filePath.first!.uppercased() + filePath.dropFirst()
71+
}
72+
#endif
73+
return filePath
6574
}
6675
} else {
6776
return storage.absoluteString

Sources/SKTestSupport/MultiFileTestProject.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ package class MultiFileTestProject {
8888
/// File contents can also contain `$TEST_DIR`, which gets replaced by the temporary directory.
8989
package init(
9090
files: [RelativeFileLocation: String],
91-
workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
91+
workspaces: (_ scratchDirectory: URL) async throws -> [WorkspaceFolder] = {
92+
[WorkspaceFolder(uri: DocumentURI($0))]
93+
},
9294
initializationOptions: LSPAny? = nil,
9395
capabilities: ClientCapabilities = ClientCapabilities(),
9496
options: SourceKitLSPOptions = .testDefault(),

Sources/SKTestSupport/SkipUnless.swift

+4
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,10 @@ package actor SkipUnless {
418418
try XCTSkipUnless(Platform.current == .darwin, message)
419419
}
420420

421+
package static func platformIsWindows(_ message: String) throws {
422+
try XCTSkipUnless(Platform.current == .windows, message)
423+
}
424+
421425
package static func platformSupportsTaskPriorityElevation() throws {
422426
#if os(macOS)
423427
guard #available(macOS 14.0, *) else {

Sources/SKTestSupport/SwiftPMTestProject.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ package class SwiftPMTestProject: MultiFileTestProject {
163163
package init(
164164
files: [RelativeFileLocation: String],
165165
manifest: String = SwiftPMTestProject.defaultPackageManifest,
166-
workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
166+
workspaces: (_ scratchDirectory: URL) async throws -> [WorkspaceFolder] = {
167+
[WorkspaceFolder(uri: DocumentURI($0))]
168+
},
167169
initializationOptions: LSPAny? = nil,
168170
capabilities: ClientCapabilities = ClientCapabilities(),
169171
options: SourceKitLSPOptions = .testDefault(),

Sources/SemanticIndex/SemanticIndexManager.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ package final actor SemanticIndexManager {
291291
filesToIndex
292292
} else {
293293
await orLog("Getting files to index") {
294-
try await self.buildSystemManager.buildableSourceFiles().keys.sorted { $0.stringValue < $1.stringValue }
294+
try await self.buildSystemManager.sourceFiles(includeNonBuildableFiles: false).keys
295+
.sorted { $0.stringValue < $1.stringValue }
295296
} ?? []
296297
}
297298
if !indexFilesWithUpToDateUnit {
@@ -408,7 +409,7 @@ package final actor SemanticIndexManager {
408409
toCover files: some Collection<DocumentURI> & Sendable
409410
) async -> [FileToIndex] {
410411
let sourceFiles = await orLog("Getting source files in project") {
411-
Set(try await buildSystemManager.buildableSourceFiles().keys)
412+
Set(try await buildSystemManager.sourceFiles(includeNonBuildableFiles: false).keys)
412413
}
413414
guard let sourceFiles else {
414415
return []

Sources/SourceKitLSP/Workspace.swift

+61
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,51 @@ fileprivate func firstNonNil<T>(
6262
return try await defaultValue()
6363
}
6464

65+
/// Actor that caches realpaths for `sourceFilesWithSameRealpath`.
66+
fileprivate actor SourceFilesWithSameRealpathInferrer {
67+
private let buildSystemManager: BuildSystemManager
68+
private var realpathCache: [DocumentURI: DocumentURI] = [:]
69+
70+
init(buildSystemManager: BuildSystemManager) {
71+
self.buildSystemManager = buildSystemManager
72+
}
73+
74+
private func realpath(of uri: DocumentURI) -> DocumentURI {
75+
if let cached = realpathCache[uri] {
76+
return cached
77+
}
78+
let value = uri.symlinkTarget ?? uri
79+
realpathCache[uri] = value
80+
return value
81+
}
82+
83+
/// Returns the URIs of all source files in the project that have the same realpath as a document in `documents` but
84+
/// are not in `documents`.
85+
///
86+
/// This is useful in the following scenario: A project has target A containing A.swift an target B containing B.swift
87+
/// B.swift is a symlink to A.swift. When A.swift is modified, both the dependencies of A and B need to be marked as
88+
/// having an out-of-date preparation status, not just A.
89+
package func sourceFilesWithSameRealpath(as documents: [DocumentURI]) async -> [DocumentURI] {
90+
let realPaths = Set(documents.map { realpath(of: $0) })
91+
return await orLog("Determining source files with same realpath") {
92+
var result: [DocumentURI] = []
93+
let filesAndDirectories = try await buildSystemManager.sourceFiles(includeNonBuildableFiles: true)
94+
for file in filesAndDirectories.keys {
95+
if realPaths.contains(realpath(of: file)) && !documents.contains(file) {
96+
result.append(file)
97+
}
98+
}
99+
return result
100+
} ?? []
101+
}
102+
103+
func filesDidChange(_ events: [FileEvent]) {
104+
for event in events {
105+
realpathCache[event.uri] = nil
106+
}
107+
}
108+
}
109+
65110
/// Represents the configuration and state of a project or combination of projects being worked on
66111
/// together.
67112
///
@@ -86,6 +131,8 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
86131
/// The build system manager to use for documents in this workspace.
87132
package let buildSystemManager: BuildSystemManager
88133

134+
private let sourceFilesWithSameRealpathInferrer: SourceFilesWithSameRealpathInferrer
135+
89136
let options: SourceKitLSPOptions
90137

91138
/// The source code index, if available.
@@ -126,6 +173,9 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
126173
self.options = options
127174
self._uncheckedIndex = ThreadSafeBox(initialValue: uncheckedIndex)
128175
self.buildSystemManager = buildSystemManager
176+
self.sourceFilesWithSameRealpathInferrer = SourceFilesWithSameRealpathInferrer(
177+
buildSystemManager: buildSystemManager
178+
)
129179
if options.backgroundIndexingOrDefault, let uncheckedIndex,
130180
await buildSystemManager.initializationData?.prepareProvider ?? false
131181
{
@@ -316,6 +366,17 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
316366
}
317367

318368
package func filesDidChange(_ events: [FileEvent]) async {
369+
// First clear any cached realpaths in `sourceFilesWithSameRealpathInferrer`.
370+
await sourceFilesWithSameRealpathInferrer.filesDidChange(events)
371+
372+
// Now infer any edits for source files that share the same realpath as one of the modified files.
373+
var events = events
374+
events +=
375+
await sourceFilesWithSameRealpathInferrer
376+
.sourceFilesWithSameRealpath(as: events.filter { $0.type == .changed }.map(\.uri))
377+
.map { FileEvent(uri: $0, type: .changed) }
378+
379+
// Notify all clients about the reported and inferred edits.
319380
await buildSystemManager.filesDidChange(events)
320381
await syntacticTestIndex.filesDidChange(events)
321382
await semanticIndexManager?.filesDidChange(events)

Sources/SwiftExtensions/URLExtensions.swift

+13-3
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,21 @@ extension URL {
6767
guard self.isFileURL else {
6868
throw FilePathError.noFileURL(self)
6969
}
70-
return try self.withUnsafeFileSystemRepresentation { buffer in
71-
guard let buffer else {
70+
return try self.withUnsafeFileSystemRepresentation { filePathPtr in
71+
guard let filePathPtr else {
7272
throw FilePathError.noFileSystemRepresentation(self)
7373
}
74-
return String(cString: buffer)
74+
let filePath = String(cString: filePathPtr)
75+
#if os(Windows)
76+
// VS Code spells file paths with a lowercase drive letter, while the rest of Windows APIs use an uppercase
77+
// drive letter. Normalize the drive letter spelling to be uppercase.
78+
if filePath.first?.isASCII ?? false, filePath.first?.isLetter ?? false, filePath.first?.isLowercase ?? false,
79+
filePath.count > 1, filePath[filePath.index(filePath.startIndex, offsetBy: 1)] == ":"
80+
{
81+
return filePath.first!.uppercased() + filePath.dropFirst()
82+
}
83+
#endif
84+
return filePath
7585
}
7686
}
7787
}

Sources/sourcekit-lsp/SourceKitLSP.swift

+12-18
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
#if compiler(>=6)
14-
public import ArgumentParser
1513
import BuildSystemIntegration
1614
import Csourcekitd // Not needed here, but fixes debugging...
1715
import Diagnose
@@ -26,24 +24,16 @@ import SourceKitLSP
2624
import SwiftExtensions
2725
import ToolchainRegistry
2826

29-
#if canImport(Android)
30-
import Android
31-
#endif
27+
import struct TSCBasic.AbsolutePath
28+
29+
#if compiler(>=6)
30+
public import ArgumentParser
3231
#else
3332
import ArgumentParser
34-
import BuildSystemIntegration
35-
import Csourcekitd // Not needed here, but fixes debugging...
36-
import Diagnose
37-
import Dispatch
38-
import Foundation
39-
import LanguageServerProtocol
40-
import LanguageServerProtocolExtensions
41-
import LanguageServerProtocolJSONRPC
42-
import SKLogging
43-
import SKOptions
44-
import SourceKitLSP
45-
import SwiftExtensions
46-
import ToolchainRegistry
33+
#endif
34+
35+
#if canImport(Android)
36+
import Android
4737
#endif
4838

4939
extension PathPrefixMapping {
@@ -279,6 +269,10 @@ struct SourceKitLSP: AsyncParsableCommand {
279269
outFD: realStdoutHandle
280270
)
281271

272+
// For reasons that are completely oblivious to me, `DispatchIO.write`, which is used to write LSP responses to
273+
// stdout fails with error code 5 on Windows unless we call `AbsolutePath(validating:)` on some URL first.
274+
_ = try AbsolutePath(validating: Bundle.main.bundlePath)
275+
282276
var inputMirror: FileHandle? = nil
283277
if let inputMirrorDirectory = globalConfigurationOptions.loggingOrDefault.inputMirrorDirectory {
284278
orLog("Setting up input mirror") {

0 commit comments

Comments
 (0)