From c0c0f1f0a65766aa463122121917f729e7697a5b Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Mon, 18 Nov 2024 15:28:34 -0800 Subject: [PATCH 01/41] Skip tests that use background indexing when running tests with a Swift 5.10 toolchain https://github.com/swiftlang/sourcekit-lsp/pull/1714 changed the background preparation mode to `enabled` but a Swift 5.10 toolchain does not support `--experimental-prepare-for-indexing`. Thus, these tests fail. Skip tests that rely on background indexing when testing SourceKit-LSP with a Swift 5.10 host toolchain. --- Sources/SKTestSupport/TestSourceKitLSPClient.swift | 3 +++ Tests/SourceKitLSPTests/BackgroundIndexingTests.swift | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift index cdd0d05ab..b902705a8 100644 --- a/Sources/SKTestSupport/TestSourceKitLSPClient.swift +++ b/Sources/SKTestSupport/TestSourceKitLSPClient.swift @@ -146,6 +146,9 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable { preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, cleanUp: @Sendable @escaping () -> Void = {} ) async throws { + if enableBackgroundIndexing { + try await SkipUnless.swiftPMSupportsExperimentalPrepareForIndexing() + } var options = options if let globalModuleCache = try globalModuleCache { options.swiftPMOrDefault.swiftCompilerFlags = diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 442ac5ec6..453bcf722 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -542,6 +542,8 @@ final class BackgroundIndexingTests: XCTestCase { } func testPrepareTargetAfterEditToDependency() async throws { + try await SkipUnless.swiftPMSupportsExperimentalPrepareForIndexing() + var testHooks = TestHooks() let expectedPreparationTracker = ExpectedIndexTaskTracker(expectedPreparations: [ [ @@ -641,6 +643,8 @@ final class BackgroundIndexingTests: XCTestCase { } func testDontStackTargetPreparationForEditorFunctionality() async throws { + try await SkipUnless.swiftPMSupportsExperimentalPrepareForIndexing() + let allDocumentsOpened = WrappedSemaphore(name: "All documents opened") let libBStartedPreparation = WrappedSemaphore(name: "LibB started preparing") let libDPreparedForEditing = WrappedSemaphore(name: "LibD prepared for editing") From be546308caa1e9958b0ac83ef75ccf96756ac921 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Mon, 18 Nov 2024 13:10:01 -0800 Subject: [PATCH 02/41] Use `URL` in many cases where we used `AbsolutePath` We made quite a few fixes recently to make sure that path handling works correctly using `URL` on Windows. Use `URL` in most places to have a single type that represents file paths instead of sometimes using `AbsolutePath`. While doing so, also remove usages of `TSCBasic.FileSystem` an `InMemoryFileSystem`. The pattern of using `InMemoryFileSystem` for tests was never consistently used and it was a little confusing that some types took a `FileSystem` parameter while other always assumed to work on the local file system. --- Package.swift | 2 +- .../BuildSystemManager.swift | 34 +- .../BuiltInBuildSystem.swift | 14 +- .../BuiltInBuildSystemAdapter.swift | 19 +- .../CompilationDatabase.swift | 61 +- .../CompilationDatabaseBuildSystem.swift | 39 +- .../DetermineBuildSystem.swift | 14 +- .../ExternalBuildSystemAdapter.swift | 57 +- .../LegacyBuildServerBuildSystem.swift | 17 +- .../SwiftPMBuildSystem.swift | 63 +- .../TestBuildSystem.swift | 14 +- Sources/Diagnose/DiagnoseCommand.swift | 12 +- Sources/Diagnose/IndexCommand.swift | 6 +- Sources/Diagnose/ReduceCommand.swift | 7 +- Sources/Diagnose/ReduceFrontendCommand.swift | 7 +- Sources/Diagnose/ReproducerBundle.swift | 2 +- .../RunSourcekitdRequestCommand.swift | 9 +- .../Diagnose/Toolchain+SwiftFrontend.swift | 2 +- .../InProcessSourceKitLSPClient.swift | 2 +- ...em.swift => FileManager+createFiles.swift} | 19 +- .../IndexedSingleSwiftFileTestProject.swift | 2 +- .../SKTestSupport/MultiFileTestProject.swift | 8 +- Sources/SKTestSupport/SkipUnless.swift | 16 +- .../SKTestSupport/SwiftPMTestProject.swift | 4 +- Sources/SKTestSupport/Utils.swift | 10 +- .../UpdateIndexStoreTaskDescription.swift | 4 +- .../DynamicallyLoadedSourceKitD.swift | 16 +- Sources/SourceKitD/SourceKitDRegistry.swift | 14 +- .../Clang/ClangLanguageService.swift | 16 +- Sources/SourceKitLSP/SourceKitLSPServer.swift | 4 +- .../Swift/DocumentFormatting.swift | 4 +- .../Swift/SwiftLanguageService.swift | 6 +- Sources/SourceKitLSP/Workspace.swift | 4 +- .../FileManagerExtensions.swift | 13 + Sources/SwiftExtensions/URLExtensions.swift | 18 +- Sources/TSCExtensions/CMakeLists.txt | 1 + .../URL+appendingRelativePath.swift | 36 + Sources/ToolchainRegistry/Toolchain.swift | 137 ++- .../ToolchainRegistry/ToolchainRegistry.swift | 72 +- .../ToolchainRegistry/XCToolchainPlist.swift | 30 +- Sources/sourcekit-lsp/SourceKitLSP.swift | 58 +- .../BuildSystemManagerTests.swift | 10 +- .../CompilationDatabaseTests.swift | 203 ++--- .../SwiftPMBuildSystemTests.swift | 346 +++++--- Tests/DiagnoseTests/DiagnoseTests.swift | 14 +- .../SourceKitDRegistryTests.swift | 28 +- .../BackgroundIndexingTests.swift | 2 +- .../SourceKitLSPTests/BuildSystemTests.swift | 2 +- .../WorkspaceTestDiscoveryTests.swift | 2 +- Tests/SourceKitLSPTests/WorkspaceTests.swift | 2 +- .../TSCExtensionsTests/ProcessRunTests.swift | 14 +- .../ToolchainRegistryTests.swift | 826 ++++++++++-------- 52 files changed, 1218 insertions(+), 1104 deletions(-) rename Sources/SKTestSupport/{FileSystem.swift => FileManager+createFiles.swift} (60%) create mode 100644 Sources/TSCExtensions/URL+appendingRelativePath.swift diff --git a/Package.swift b/Package.swift index af3560dac..aa13fdd92 100644 --- a/Package.swift +++ b/Package.swift @@ -97,6 +97,7 @@ var targets: [Target] = [ "SKTestSupport", "SourceKitLSP", "ToolchainRegistry", + "TSCExtensions", ], swiftSettings: globalSwiftSettings ), @@ -348,7 +349,6 @@ var targets: [Target] = [ "Csourcekitd", "SKLogging", "SwiftExtensions", - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), ], exclude: ["CMakeLists.txt", "sourcekitd_uids.swift.gyb"], swiftSettings: globalSwiftSettings diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 2acac7860..801c9e22f 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -13,7 +13,7 @@ #if compiler(>=6) package import BuildServerProtocol import Dispatch -import Foundation +package import Foundation package import LanguageServerProtocol package import LanguageServerProtocolExtensions import SKLogging @@ -23,8 +23,7 @@ package import SwiftExtensions package import ToolchainRegistry import TSCExtensions -package import struct TSCBasic.AbsolutePath -package import struct TSCBasic.RelativePath +import struct TSCBasic.RelativePath #else import BuildServerProtocol import Dispatch @@ -38,7 +37,6 @@ import SwiftExtensions import ToolchainRegistry import TSCExtensions -import struct TSCBasic.AbsolutePath import struct TSCBasic.RelativePath #endif @@ -138,13 +136,13 @@ private enum BuildSystemAdapter { private extension BuildSystemSpec { private static func createBuiltInBuildSystemAdapter( - projectRoot: AbsolutePath, + projectRoot: URL, messagesToSourceKitLSPHandler: any MessageHandler, buildSystemTestHooks: BuildSystemTestHooks, _ createBuildSystem: @Sendable (_ connectionToSourceKitLSP: any Connection) async throws -> BuiltInBuildSystem? ) async -> BuildSystemAdapter? { let connectionToSourceKitLSP = LocalConnection( - receiverName: "BuildSystemManager for \(projectRoot.asURL.lastPathComponent)" + receiverName: "BuildSystemManager for \(projectRoot.lastPathComponent)" ) connectionToSourceKitLSP.start(handler: messagesToSourceKitLSPHandler) @@ -152,17 +150,17 @@ private extension BuildSystemSpec { try await createBuildSystem(connectionToSourceKitLSP) } guard let buildSystem else { - logger.log("Failed to create build system at \(projectRoot.pathString)") + logger.log("Failed to create build system at \(projectRoot)") return nil } - logger.log("Created \(type(of: buildSystem), privacy: .public) at \(projectRoot.pathString)") + logger.log("Created \(type(of: buildSystem), privacy: .public) at \(projectRoot)") let buildSystemAdapter = BuiltInBuildSystemAdapter( underlyingBuildSystem: buildSystem, connectionToSourceKitLSP: connectionToSourceKitLSP, buildSystemTestHooks: buildSystemTestHooks ) let connectionToBuildSystem = LocalConnection( - receiverName: "\(type(of: buildSystem)) for \(projectRoot.asURL.lastPathComponent)" + receiverName: "\(type(of: buildSystem)) for \(projectRoot.lastPathComponent)" ) connectionToBuildSystem.start(handler: buildSystemAdapter) return .builtIn(buildSystemAdapter, connectionToBuildSystem: connectionToBuildSystem) @@ -185,10 +183,10 @@ private extension BuildSystemSpec { ) } guard let buildSystem else { - logger.log("Failed to create external build system at \(projectRoot.pathString)") + logger.log("Failed to create external build system at \(projectRoot)") return nil } - logger.log("Created external build server at \(projectRoot.pathString)") + logger.log("Created external build server at \(projectRoot)") return .external(buildSystem) case .compilationDatabase: return await Self.createBuiltInBuildSystemAdapter( @@ -245,7 +243,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler { /// For compilation databases it is the root folder based on which the compilation database was found. /// /// `nil` if the `BuildSystemManager` does not have an underlying build system. - package let projectRoot: AbsolutePath? + package let projectRoot: URL? /// The files for which the delegate has requested change notifications, ie. the files for which the delegate wants to /// get `fileBuildSettingsChanged` and `filesDependenciesUpdated` callbacks. @@ -402,7 +400,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler { displayName: "SourceKit-LSP", version: "", bspVersion: "2.2.0", - rootUri: URI(buildSystemSpec.projectRoot.asURL), + rootUri: URI(buildSystemSpec.projectRoot), capabilities: BuildClientCapabilities(languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift]) ) ) @@ -608,7 +606,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler { in target: BuildTargetIdentifier?, language: Language ) async -> Toolchain? { - let toolchainPath = await orLog("Getting toolchain from build targets") { () -> AbsolutePath? in + let toolchainPath = await orLog("Getting toolchain from build targets") { () -> URL? in guard let target else { return nil } @@ -624,7 +622,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler { logger.error("Toolchain is not a file URL") return nil } - return try AbsolutePath(validating: toolchainUrl.filePath) + return toolchainUrl } if let toolchainPath { if let toolchain = await self.toolchainRegistry.toolchain(withPath: toolchainPath) { @@ -681,11 +679,9 @@ package actor BuildSystemManager: QueueBasedMessageHandler { if let targets = filesAndDirectories.files[document]?.targets { result.formUnion(targets) } - if !filesAndDirectories.directories.isEmpty, - let documentPath = AbsolutePath(validatingOrNil: try? document.fileURL?.filePath) - { + if !filesAndDirectories.directories.isEmpty, let documentPath = document.fileURL { for (directory, info) in filesAndDirectories.directories { - guard let directoryPath = AbsolutePath(validatingOrNil: try? directory.fileURL?.filePath) else { + guard let directoryPath = directory.fileURL else { continue } if documentPath.isDescendant(of: directoryPath) { diff --git a/Sources/BuildSystemIntegration/BuiltInBuildSystem.swift b/Sources/BuildSystemIntegration/BuiltInBuildSystem.swift index 03313a9ae..6906264b9 100644 --- a/Sources/BuildSystemIntegration/BuiltInBuildSystem.swift +++ b/Sources/BuildSystemIntegration/BuiltInBuildSystem.swift @@ -12,22 +12,18 @@ #if compiler(>=6) package import BuildServerProtocol +package import Foundation package import LanguageServerProtocol import SKLogging import SKOptions import ToolchainRegistry - -package import struct TSCBasic.AbsolutePath -package import struct TSCBasic.RelativePath #else import BuildServerProtocol +import Foundation import LanguageServerProtocol import SKLogging import SKOptions import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import struct TSCBasic.RelativePath #endif /// An error build systems can throw from `prepare` if they don't support preparation of targets. @@ -42,16 +38,16 @@ package protocol BuiltInBuildSystem: AnyObject, Sendable { /// The root of the project that this build system manages. For example, for SwiftPM packages, this is the folder /// containing Package.swift. For compilation databases it is the root folder based on which the compilation database /// was found. - var projectRoot: AbsolutePath { get async } + var projectRoot: URL { get async } /// The files to watch for changes. var fileWatchers: [FileSystemWatcher] { get async } /// The path to the raw index store data, if any. - var indexStorePath: AbsolutePath? { get async } + var indexStorePath: URL? { get async } /// The path to put the index database, if any. - var indexDatabasePath: AbsolutePath? { get async } + var indexDatabasePath: URL? { get async } /// Whether the build system is capable of preparing a target for indexing, ie. if the `prepare` methods has been /// implemented. diff --git a/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift b/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift index 9f9ac2200..8c86974cb 100644 --- a/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift +++ b/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift @@ -11,7 +11,6 @@ //===----------------------------------------------------------------------===// import BuildServerProtocol -import Foundation import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging @@ -20,11 +19,9 @@ import SwiftExtensions import ToolchainRegistry #if compiler(>=6) -package import struct TSCBasic.AbsolutePath -package import struct TSCBasic.RelativePath +package import Foundation #else -import struct TSCBasic.AbsolutePath -import struct TSCBasic.RelativePath +import Foundation #endif /// The details necessary to create a `BuildSystemAdapter`. @@ -38,9 +35,9 @@ package struct BuildSystemSpec { package var kind: Kind - package var projectRoot: AbsolutePath + package var projectRoot: URL - package init(kind: BuildSystemSpec.Kind, projectRoot: AbsolutePath) { + package init(kind: BuildSystemSpec.Kind, projectRoot: URL) { self.kind = kind self.projectRoot = projectRoot } @@ -95,8 +92,12 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler { capabilities: BuildServerCapabilities(), dataKind: .sourceKit, data: SourceKitInitializeBuildResponseData( - indexDatabasePath: await underlyingBuildSystem.indexDatabasePath?.pathString, - indexStorePath: await underlyingBuildSystem.indexStorePath?.pathString, + indexDatabasePath: await orLog("getting index database file path") { + try await underlyingBuildSystem.indexDatabasePath?.filePath + }, + indexStorePath: await orLog("getting index store file path") { + try await underlyingBuildSystem.indexStorePath?.filePath + }, watchers: await underlyingBuildSystem.fileWatchers, prepareProvider: underlyingBuildSystem.supportsPreparation, sourceKitOptionsProvider: true diff --git a/Sources/BuildSystemIntegration/CompilationDatabase.swift b/Sources/BuildSystemIntegration/CompilationDatabase.swift index a3521ffa1..2297c03d8 100644 --- a/Sources/BuildSystemIntegration/CompilationDatabase.swift +++ b/Sources/BuildSystemIntegration/CompilationDatabase.swift @@ -12,17 +12,14 @@ #if compiler(>=6) package import BuildServerProtocol -import Foundation +package import Foundation package import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging import SwiftExtensions import TSCExtensions -package import struct TSCBasic.AbsolutePath -package import protocol TSCBasic.FileSystem package import struct TSCBasic.RelativePath -package import var TSCBasic.localFileSystem #else import BuildServerProtocol import Foundation @@ -32,10 +29,7 @@ import SKLogging import SwiftExtensions import TSCExtensions -import struct TSCBasic.AbsolutePath -import protocol TSCBasic.FileSystem import struct TSCBasic.RelativePath -import var TSCBasic.localFileSystem #endif #if os(Windows) @@ -124,9 +118,8 @@ package protocol CompilationDatabase { /// Loads the compilation database located in `directory`, if one can be found in `additionalSearchPaths` or in the default search paths of "." and "build". package func tryLoadCompilationDatabase( - directory: AbsolutePath, - additionalSearchPaths: [RelativePath] = [], - _ fileSystem: FileSystem = localFileSystem + directory: URL, + additionalSearchPaths: [RelativePath] = [] ) -> CompilationDatabase? { let searchPaths = additionalSearchPaths + [ @@ -140,10 +133,10 @@ package func tryLoadCompilationDatabase( .map { directory.appending($0) } .compactMap { searchPath in orLog("Failed to load compilation database") { () -> CompilationDatabase? in - if let compDb = try JSONCompilationDatabase(directory: searchPath, fileSystem) { + if let compDb = try JSONCompilationDatabase(directory: searchPath) { return compDb } - if let compDb = try FixedCompilationDatabase(directory: searchPath, fileSystem) { + if let compDb = try FixedCompilationDatabase(directory: searchPath) { return compDb } return nil @@ -178,30 +171,26 @@ package struct FixedCompilationDatabase: CompilationDatabase, Equatable { /// Loads the compilation database located in `directory`, if any. /// - Returns: `nil` if `compile_flags.txt` was not found - package init?(directory: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { - let path = directory.appending(component: "compile_flags.txt") - try self.init(file: path, fileSystem) + package init?(directory: URL) throws { + let path = directory.appendingPathComponent("compile_flags.txt") + try self.init(file: path) } /// Loads the compilation database from `file` /// - Returns: `nil` if the file does not exist - package init?(file: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { - self.directory = file.dirname + package init?(file: URL) throws { + self.directory = try file.deletingLastPathComponent().filePath - guard fileSystem.exists(file) else { + let fileContents: String + do { + fileContents = try String(contentsOf: file, encoding: .utf8) + } catch { return nil } - let bytes = try fileSystem.readFileContents(file) var fixedArgs: [String] = ["clang"] - try bytes.withUnsafeData { data in - guard let fileContents = String(data: data, encoding: .utf8) else { - throw CompilationDatabaseDecodingError.fixedDatabaseDecodingError - } - - fileContents.enumerateLines { line, _ in - fixedArgs.append(line.trimmingCharacters(in: .whitespacesAndNewlines)) - } + fileContents.enumerateLines { line, _ in + fixedArgs.append(line.trimmingCharacters(in: .whitespacesAndNewlines)) } self.fixedArgs = fixedArgs } @@ -242,21 +231,21 @@ package struct JSONCompilationDatabase: CompilationDatabase, Equatable, Codable /// Loads the compilation database located in `directory`, if any. /// /// - Returns: `nil` if `compile_commands.json` was not found - package init?(directory: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { - let path = directory.appending(component: "compile_commands.json") - try self.init(file: path, fileSystem) + package init?(directory: URL) throws { + let path = directory.appendingPathComponent("compile_commands.json") + try self.init(file: path) } /// Loads the compilation database from `file` /// - Returns: `nil` if the file does not exist - package init?(file: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { - guard fileSystem.exists(file) else { + package init?(file: URL) throws { + let data: Data + do { + data = try Data(contentsOf: file) + } catch { return nil } - let bytes = try fileSystem.readFileContents(file) - try bytes.withUnsafeData { data in - self = try JSONDecoder().decode(JSONCompilationDatabase.self, from: data) - } + self = try JSONDecoder().decode(JSONCompilationDatabase.self, from: data) } package func encode(to encoder: Encoder) throws { diff --git a/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift b/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift index c84f2be61..dffa9a720 100644 --- a/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift +++ b/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift @@ -13,6 +13,7 @@ #if compiler(>=6) package import BuildServerProtocol import Dispatch +package import Foundation package import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging @@ -20,14 +21,11 @@ package import SKOptions import ToolchainRegistry import TSCExtensions -import struct Foundation.URL -package import struct TSCBasic.AbsolutePath -package import protocol TSCBasic.FileSystem package import struct TSCBasic.RelativePath -package import var TSCBasic.localFileSystem #else import BuildServerProtocol import Dispatch +import Foundation import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging @@ -35,11 +33,7 @@ import SKOptions import ToolchainRegistry import TSCExtensions -import struct Foundation.URL -import struct TSCBasic.AbsolutePath -import protocol TSCBasic.FileSystem import struct TSCBasic.RelativePath -import var TSCBasic.localFileSystem #endif fileprivate enum Cachable { @@ -67,7 +61,7 @@ fileprivate enum Cachable { /// Provides build settings from a `CompilationDatabase` found by searching a project. For now, only /// one compilation database, located at the project root. package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem { - static package func projectRoot(for workspaceFolder: AbsolutePath, options: SourceKitLSPOptions) -> AbsolutePath? { + static package func projectRoot(for workspaceFolder: URL, options: SourceKitLSPOptions) -> URL? { if tryLoadCompilationDatabase(directory: workspaceFolder) != nil { return workspaceFolder } @@ -85,17 +79,16 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem { private let connectionToSourceKitLSP: any Connection private let searchPaths: [RelativePath] - private let fileSystem: FileSystem - package let projectRoot: AbsolutePath + package let projectRoot: URL package let fileWatchers: [FileSystemWatcher] = [ FileSystemWatcher(globPattern: "**/compile_commands.json", kind: [.create, .change, .delete]), FileSystemWatcher(globPattern: "**/compile_flags.txt", kind: [.create, .change, .delete]), ] - private var _indexStorePath: Cachable = .noValue - package var indexStorePath: AbsolutePath? { + private var _indexStorePath: Cachable = .noValue + package var indexStorePath: URL? { _indexStorePath.get { guard let compdb else { return nil @@ -106,7 +99,7 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem { let args = command.commandLine for i in args.indices.reversed() { if args[i] == "-index-store-path" && i + 1 < args.count { - return AbsolutePath(validatingOrNil: args[i + 1]) + return URL(fileURLWithPath: args[i + 1]) } } } @@ -115,23 +108,21 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem { } } - package var indexDatabasePath: AbsolutePath? { - indexStorePath?.parentDirectory.appending(component: "IndexDatabase") + package var indexDatabasePath: URL? { + indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase") } package nonisolated var supportsPreparation: Bool { false } package init?( - projectRoot: AbsolutePath, + projectRoot: URL, searchPaths: [RelativePath], - connectionToSourceKitLSP: any Connection, - fileSystem: FileSystem = localFileSystem + connectionToSourceKitLSP: any Connection ) { - self.fileSystem = fileSystem self.projectRoot = projectRoot self.searchPaths = searchPaths self.connectionToSourceKitLSP = connectionToSourceKitLSP - if let compdb = tryLoadCompilationDatabase(directory: projectRoot, additionalSearchPaths: searchPaths, fileSystem) { + if let compdb = tryLoadCompilationDatabase(directory: projectRoot, additionalSearchPaths: searchPaths) { self.compdb = compdb } else { return nil @@ -198,11 +189,7 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem { /// The compilation database has been changed on disk. /// Reload it and notify the delegate about build setting changes. private func reloadCompilationDatabase() { - self.compdb = tryLoadCompilationDatabase( - directory: projectRoot, - additionalSearchPaths: searchPaths, - self.fileSystem - ) + self.compdb = tryLoadCompilationDatabase(directory: projectRoot, additionalSearchPaths: searchPaths) connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) } diff --git a/Sources/BuildSystemIntegration/DetermineBuildSystem.swift b/Sources/BuildSystemIntegration/DetermineBuildSystem.swift index e564f93a2..fabf33783 100644 --- a/Sources/BuildSystemIntegration/DetermineBuildSystem.swift +++ b/Sources/BuildSystemIntegration/DetermineBuildSystem.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +import Foundation + #if compiler(>=6) package import LanguageServerProtocol import SKLogging @@ -43,25 +45,21 @@ package func determineBuildSystem( buildSystemPreference.removeAll(where: { $0 == defaultBuildSystem }) buildSystemPreference.insert(defaultBuildSystem, at: 0) } - guard let workspaceFolderUrl = workspaceFolder.fileURL, - let workspaceFolderPath = try? AbsolutePath(validating: workspaceFolderUrl.filePath) - else { + guard let workspaceFolderUrl = workspaceFolder.fileURL else { return nil } for buildSystemType in buildSystemPreference { switch buildSystemType { case .buildServer: - if let projectRoot = ExternalBuildSystemAdapter.projectRoot(for: workspaceFolderPath, options: options) { + if let projectRoot = ExternalBuildSystemAdapter.projectRoot(for: workspaceFolderUrl, options: options) { return BuildSystemSpec(kind: .buildServer, projectRoot: projectRoot) } case .compilationDatabase: - if let projectRoot = CompilationDatabaseBuildSystem.projectRoot(for: workspaceFolderPath, options: options) { + if let projectRoot = CompilationDatabaseBuildSystem.projectRoot(for: workspaceFolderUrl, options: options) { return BuildSystemSpec(kind: .compilationDatabase, projectRoot: projectRoot) } case .swiftPM: - if let projectRootURL = SwiftPMBuildSystem.projectRoot(for: workspaceFolderUrl, options: options), - let projectRoot = try? AbsolutePath(validating: projectRootURL.filePath) - { + if let projectRoot = SwiftPMBuildSystem.projectRoot(for: workspaceFolderUrl, options: options) { return BuildSystemSpec(kind: .swiftPM, projectRoot: projectRoot) } } diff --git a/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift b/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift index bc8b1cca2..cbb0586ef 100644 --- a/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift +++ b/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift @@ -20,7 +20,6 @@ import SKOptions import SwiftExtensions import TSCExtensions -import struct TSCBasic.AbsolutePath import func TSCBasic.getEnvSearchPaths import var TSCBasic.localFileSystem import func TSCBasic.lookupExecutablePath @@ -34,7 +33,7 @@ private func executable(_ name: String) -> String { #endif } -private let python3ExecutablePath: AbsolutePath? = { +private let python3ExecutablePath: URL? = { let pathVariable: String #if os(Windows) pathVariable = "Path" @@ -47,8 +46,8 @@ private let python3ExecutablePath: AbsolutePath? = { currentWorkingDirectory: localFileSystem.currentWorkingDirectory ) - return lookupExecutablePath(filename: executable("python3"), searchPaths: searchPaths) - ?? lookupExecutablePath(filename: executable("python"), searchPaths: searchPaths) + return lookupExecutablePath(filename: executable("python3"), searchPaths: searchPaths)?.asURL + ?? lookupExecutablePath(filename: executable("python"), searchPaths: searchPaths)?.asURL }() struct ExecutableNotFoundError: Error { @@ -75,16 +74,16 @@ private struct BuildServerConfig: Codable { /// Command arguments runnable via system processes to start a BSP server. let argv: [String] - static func load(from path: AbsolutePath) throws -> BuildServerConfig { + static func load(from path: URL) throws -> BuildServerConfig { let decoder = JSONDecoder() - let fileData = try localFileSystem.readFileContents(path).contents - return try decoder.decode(BuildServerConfig.self, from: Data(fileData)) + let fileData = try Data(contentsOf: path) + return try decoder.decode(BuildServerConfig.self, from: fileData) } } /// Launches a subprocess that is a BSP server and manages the process's lifetime. actor ExternalBuildSystemAdapter { - private let projectRoot: AbsolutePath + private let projectRoot: URL /// The `BuildSystemManager` that handles messages from the BSP server to SourceKit-LSP. var messagesToSourceKitLSPHandler: MessageHandler @@ -100,7 +99,7 @@ actor ExternalBuildSystemAdapter { /// Used to delay restarting in case of a crash loop. private var lastRestart: Date? - static package func projectRoot(for workspaceFolder: AbsolutePath, options: SourceKitLSPOptions) -> AbsolutePath? { + static package func projectRoot(for workspaceFolder: URL, options: SourceKitLSPOptions) -> URL? { guard getConfigPath(for: workspaceFolder) != nil else { return nil } @@ -108,7 +107,7 @@ actor ExternalBuildSystemAdapter { } init( - projectRoot: AbsolutePath, + projectRoot: URL, messagesToSourceKitLSPHandler: MessageHandler ) async throws { self.projectRoot = projectRoot @@ -154,11 +153,11 @@ actor ExternalBuildSystemAdapter { } let serverConfig = try BuildServerConfig.load(from: configPath) - var serverPath = try AbsolutePath(validating: serverConfig.argv[0], relativeTo: projectRoot) + var serverPath = URL(fileURLWithPath: serverConfig.argv[0], relativeTo: projectRoot.ensuringCorrectTrailingSlash) var serverArgs = Array(serverConfig.argv[1...]) - if serverPath.suffix == ".py" { - serverArgs = [serverPath.pathString] + serverArgs + if serverPath.pathExtension == "py" { + serverArgs = [try serverPath.filePath] + serverArgs guard let interpreterPath = python3ExecutablePath else { throw ExecutableNotFoundError(executableName: "python3") } @@ -167,7 +166,7 @@ actor ExternalBuildSystemAdapter { } return try JSONRPCConnection.start( - executable: serverPath.asURL, + executable: serverPath, arguments: serverArgs, name: "BSP-Server", protocol: bspRegistry, @@ -188,10 +187,10 @@ actor ExternalBuildSystemAdapter { ).connection } - private static func getConfigPath(for workspaceFolder: AbsolutePath? = nil) -> AbsolutePath? { + private static func getConfigPath(for workspaceFolder: URL? = nil) -> URL? { var buildServerConfigLocations: [URL?] = [] if let workspaceFolder = workspaceFolder { - buildServerConfigLocations.append(workspaceFolder.appending(component: ".bsp").asURL) + buildServerConfigLocations.append(workspaceFolder.appendingPathComponent(".bsp")) } #if os(Windows) @@ -226,19 +225,17 @@ actor ExternalBuildSystemAdapter { try? FileManager.default.contentsOfDirectory(at: buildServerConfigLocation, includingPropertiesForKeys: nil) .filter { $0.pathExtension == "json" } - if let configFileURL = jsonFiles?.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }).first, - let configFilePath = AbsolutePath(validatingOrNil: try? configFileURL.filePath) - { - return configFilePath + if let configFileURL = jsonFiles?.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }).first { + return configFileURL } } // Pre Swift 6.1 SourceKit-LSP looked for `buildServer.json` in the project root. Maintain this search location for // compatibility even though it's not a standard BSP search location. - if let workspaceFolder = workspaceFolder, - localFileSystem.isFile(workspaceFolder.appending(component: "buildServer.json")) + if let buildServerPath = workspaceFolder?.appendingPathComponent("buildServer.json"), + FileManager.default.isFile(at: buildServerPath) { - return workspaceFolder.appending(component: "buildServer.json") + return buildServerPath } return nil @@ -281,3 +278,17 @@ actor ExternalBuildSystemAdapter { self.messagesToSourceKitLSPHandler.handle(OnBuildTargetDidChangeNotification(changes: nil)) } } + +fileprivate extension URL { + /// If the path of this URL represents a directory, ensure that it has a trailing slash. + /// + /// This is important because if we form a file URL relative to eg. file:///tmp/a would assumes that `a` is a file + /// and use `/tmp` as the base, not `/tmp/a`. + var ensuringCorrectTrailingSlash: URL { + guard self.isFileURL else { + return self + } + // `URL(fileURLWithPath:)` checks the file system to decide whether a directory exists at the path. + return URL(fileURLWithPath: self.path) + } +} diff --git a/Sources/BuildSystemIntegration/LegacyBuildServerBuildSystem.swift b/Sources/BuildSystemIntegration/LegacyBuildServerBuildSystem.swift index fb46c49f7..d097798b8 100644 --- a/Sources/BuildSystemIntegration/LegacyBuildServerBuildSystem.swift +++ b/Sources/BuildSystemIntegration/LegacyBuildServerBuildSystem.swift @@ -20,13 +20,6 @@ import SKOptions import SwiftExtensions import ToolchainRegistry -import struct TSCBasic.AbsolutePath -import protocol TSCBasic.FileSystem -import struct TSCBasic.FileSystemError -import func TSCBasic.getEnvSearchPaths -import var TSCBasic.localFileSystem -import func TSCBasic.lookupExecutablePath - #if compiler(>=6.3) #warning("We have had a one year transition period to the pull based build server. Consider removing this build server") #endif @@ -53,12 +46,12 @@ actor LegacyBuildServerBuildSystem: MessageHandler, BuiltInBuildSystem { /// execution of tasks. private let bspMessageHandlingQueue = AsyncQueue() - package let projectRoot: AbsolutePath + package let projectRoot: URL var fileWatchers: [FileSystemWatcher] = [] - let indexDatabasePath: AbsolutePath? - let indexStorePath: AbsolutePath? + let indexDatabasePath: URL? + let indexStorePath: URL? package let connectionToSourceKitLSP: LocalConnection @@ -69,7 +62,7 @@ actor LegacyBuildServerBuildSystem: MessageHandler, BuiltInBuildSystem { private var urisRegisteredForChanges: Set = [] init( - projectRoot: AbsolutePath, + projectRoot: URL, initializationData: InitializeBuildResponse, _ externalBuildSystemAdapter: ExternalBuildSystemAdapter ) async { @@ -163,7 +156,7 @@ actor LegacyBuildServerBuildSystem: MessageHandler, BuiltInBuildSystem { return BuildTargetSourcesResponse(items: [ SourcesItem( target: .dummy, - sources: [SourceItem(uri: DocumentURI(self.projectRoot.asURL), kind: .directory, generated: false)] + sources: [SourceItem(uri: DocumentURI(self.projectRoot), kind: .directory, generated: false)] ) ]) } diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 6f58cdfcb..85d57e2f9 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// #if compiler(>=6) -package import Basics +import Basics @preconcurrency import Build package import BuildServerProtocol import Dispatch @@ -31,13 +31,8 @@ package import ToolchainRegistry import TSCExtensions @preconcurrency import Workspace -package import struct Basics.AbsolutePath -package import struct Basics.IdentifiableSet -package import struct Basics.TSCAbsolutePath import struct TSCBasic.AbsolutePath -import protocol TSCBasic.FileSystem import class TSCBasic.Process -import var TSCBasic.localFileSystem package import class ToolchainRegistry.Toolchain #else import Basics @@ -60,14 +55,8 @@ import ToolchainRegistry import TSCExtensions @preconcurrency import Workspace -import struct Basics.AbsolutePath -import struct Basics.IdentifiableSet -import struct Basics.TSCAbsolutePath -import struct Foundation.URL import struct TSCBasic.AbsolutePath -import protocol TSCBasic.FileSystem import class TSCBasic.Process -import var TSCBasic.localFileSystem import class ToolchainRegistry.Toolchain #endif @@ -214,7 +203,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { // MARK: Build system options (set once and not modified) /// The directory containing `Package.swift`. - package let projectRoot: TSCAbsolutePath + package let projectRoot: URL package let fileWatchers: [FileSystemWatcher] @@ -244,7 +233,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { return nil } while true { - let packagePath = path.appending(component: "Package.swift") + let packagePath = path.appendingPathComponent("Package.swift") if (try? String(contentsOf: packagePath, encoding: .utf8))?.contains("PackageDescription") ?? false { return path } @@ -265,7 +254,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { /// manifest parsing and runtime support. /// - Throws: If there is an error loading the package, or no manifest is found. package init( - projectRoot: TSCAbsolutePath, + projectRoot: URL, toolchainRegistry: ToolchainRegistry, options: SourceKitLSPOptions, connectionToSourceKitLSP: any Connection, @@ -274,8 +263,8 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { self.projectRoot = projectRoot self.options = options self.fileWatchers = - ["Package.swift", "Package.resolved"].map { - FileSystemWatcher(globPattern: projectRoot.appending(component: $0).pathString, kind: [.change]) + try ["Package.swift", "Package.resolved"].map { + FileSystemWatcher(globPattern: try projectRoot.appendingPathComponent($0).filePath, kind: [.change]) } + FileRuleDescription.builtinRules.flatMap({ $0.fileTypes }).map { fileExtension in FileSystemWatcher(globPattern: "**/*.\(fileExtension)", kind: [.create, .change, .delete]) @@ -291,11 +280,11 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { self.testHooks = testHooks self.connectionToSourceKitLSP = connectionToSourceKitLSP - guard let destinationToolchainBinDir = toolchain.swiftc?.parentDirectory else { + guard let destinationToolchainBinDir = toolchain.swiftc?.deletingLastPathComponent() else { throw Error.cannotDetermineHostToolchain } - let hostSDK = try SwiftSDK.hostSwiftSDK(AbsolutePath(destinationToolchainBinDir)) + let hostSDK = try SwiftSDK.hostSwiftSDK(AbsolutePath(validating: destinationToolchainBinDir.filePath)) let hostSwiftPMToolchain = try UserToolchain(swiftSDK: hostSDK) let destinationSDK = try SwiftSDK.deriveTargetSwiftSDK( @@ -318,11 +307,13 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { let destinationSwiftPMToolchain = try UserToolchain(swiftSDK: destinationSDK) var location = try Workspace.Location( - forRootPackage: AbsolutePath(projectRoot), + forRootPackage: try AbsolutePath(validating: projectRoot.filePath), fileSystem: localFileSystem ) if options.backgroundIndexingOrDefault { - location.scratchDirectory = AbsolutePath(projectRoot.appending(components: ".build", "index-build")) + location.scratchDirectory = try AbsolutePath( + validating: projectRoot.appendingPathComponent(".build").appendingPathComponent("index-build").filePath + ) } else if let scratchDirectory = options.swiftPMOrDefault.scratchPath, let scratchDirectoryPath = try? AbsolutePath(validating: scratchDirectory) { @@ -413,7 +404,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { } let modulesGraph = try await self.swiftPMWorkspace.loadPackageGraph( - rootInput: PackageGraphRootInput(packages: [AbsolutePath(projectRoot)]), + rootInput: PackageGraphRootInput(packages: [AbsolutePath(validating: projectRoot.filePath)]), forceResolvedVersions: !isForIndexBuild, observabilityScope: observabilitySystem.topScope ) @@ -454,17 +445,19 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { package nonisolated var supportsPreparation: Bool { true } - package var buildPath: TSCAbsolutePath { - return TSCAbsolutePath(destinationBuildParameters.buildPath) + package var buildPath: URL { + return destinationBuildParameters.buildPath.asURL } - package var indexStorePath: TSCAbsolutePath? { - return destinationBuildParameters.indexStoreMode == .off - ? nil : TSCAbsolutePath(destinationBuildParameters.indexStore) + package var indexStorePath: URL? { + if destinationBuildParameters.indexStoreMode == .off { + return nil + } + return destinationBuildParameters.indexStore.asURL } - package var indexDatabasePath: TSCAbsolutePath? { - return buildPath.appending(components: "index", "db") + package var indexDatabasePath: URL? { + return buildPath.appendingPathComponent("index").appendingPathComponent("db") } /// Return the compiler arguments for the given source file within a target, making any necessary adjustments to @@ -518,7 +511,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift], dependencies: self.targetDependencies[targetId, default: []].sorted { $0.uri.stringValue < $1.uri.stringValue }, dataKind: .sourceKit, - data: SourceKitBuildTarget(toolchain: toolchain.path?.asURI).encodeToLSPAny() + data: SourceKitBuildTarget(toolchain: toolchain.path.map(URI.init)).encodeToLSPAny() ) } targets.append( @@ -546,7 +539,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { target: target, sources: [ SourceItem( - uri: projectRoot.appending(component: "Package.swift").asURI, + uri: DocumentURI(projectRoot.appendingPathComponent("Package.swift")), kind: .file, generated: false ) @@ -604,7 +597,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { // with the `.cpp` file. let buildSettings = FileBuildSettings( compilerArguments: try await compilerArguments(for: DocumentURI(substituteFile), in: swiftPMTarget), - workingDirectory: projectRoot.pathString + workingDirectory: try projectRoot.filePath ).patching(newFile: DocumentURI(try path.asURL.realpath), originalFile: DocumentURI(substituteFile)) return TextDocumentSourceKitOptionsResponse( compilerArguments: buildSettings.compilerArguments, @@ -614,7 +607,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { return TextDocumentSourceKitOptionsResponse( compilerArguments: try await compilerArguments(for: request.textDocument.uri, in: swiftPMTarget), - workingDirectory: projectRoot.pathString + workingDirectory: try projectRoot.filePath ) } @@ -651,8 +644,8 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { } logger.debug("Preparing '\(target.forLogging)' using \(self.toolchain.identifier)") var arguments = [ - swift.pathString, "build", - "--package-path", projectRoot.pathString, + try swift.filePath, "build", + "--package-path", try projectRoot.filePath, "--scratch-path", self.swiftPMWorkspace.location.scratchDirectory.pathString, "--disable-index-store", "--target", try target.targetProperties.target, diff --git a/Sources/BuildSystemIntegration/TestBuildSystem.swift b/Sources/BuildSystemIntegration/TestBuildSystem.swift index 3c1d7e5c6..aaba29626 100644 --- a/Sources/BuildSystemIntegration/TestBuildSystem.swift +++ b/Sources/BuildSystemIntegration/TestBuildSystem.swift @@ -12,29 +12,27 @@ #if compiler(>=6) package import BuildServerProtocol +package import Foundation package import LanguageServerProtocol import SKOptions import ToolchainRegistry - -package import struct TSCBasic.AbsolutePath #else import BuildServerProtocol +import Foundation import LanguageServerProtocol import SKOptions import ToolchainRegistry - -import struct TSCBasic.AbsolutePath #endif /// Build system to be used for testing BuildSystem and BuildSystemDelegate functionality with SourceKitLSPServer /// and other components. package actor TestBuildSystem: BuiltInBuildSystem { - package let projectRoot: AbsolutePath + package let projectRoot: URL package let fileWatchers: [FileSystemWatcher] = [] - package let indexStorePath: AbsolutePath? = nil - package let indexDatabasePath: AbsolutePath? = nil + package let indexStorePath: URL? = nil + package let indexDatabasePath: URL? = nil private let connectionToSourceKitLSP: any Connection @@ -49,7 +47,7 @@ package actor TestBuildSystem: BuiltInBuildSystem { package nonisolated var supportsPreparation: Bool { false } package init( - projectRoot: AbsolutePath, + projectRoot: URL, connectionToSourceKitLSP: any Connection ) { self.projectRoot = projectRoot diff --git a/Sources/Diagnose/DiagnoseCommand.swift b/Sources/Diagnose/DiagnoseCommand.swift index d5654647f..16e207856 100644 --- a/Sources/Diagnose/DiagnoseCommand.swift +++ b/Sources/Diagnose/DiagnoseCommand.swift @@ -97,7 +97,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { var toolchainRegistry: ToolchainRegistry { get throws { - let installPath = try AbsolutePath(validating: Bundle.main.bundlePath) + let installPath = Bundle.main.bundleURL return ToolchainRegistry(installPath: installPath) } } @@ -106,7 +106,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { var toolchain: Toolchain? { get async throws { if let toolchainOverride { - return Toolchain(try AbsolutePath(validating: toolchainOverride)) + return Toolchain(URL(fileURLWithPath: toolchainOverride)) } return try await toolchainRegistry.default } @@ -178,14 +178,14 @@ package struct DiagnoseCommand: AsyncParsableCommand { .deletingLastPathComponent() .deletingLastPathComponent() - guard let toolchain = try Toolchain(AbsolutePath(validating: toolchainPath.filePath)), + guard let toolchain = Toolchain(toolchainPath), let sourcekitd = toolchain.sourcekitd else { continue } let executor = OutOfProcessSourceKitRequestExecutor( - sourcekitd: sourcekitd.asURL, + sourcekitd: sourcekitd, swiftFrontend: crashInfo.swiftFrontend, reproducerPredicate: nil ) @@ -335,7 +335,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { message: "Determining Swift version of \(toolchain.identifier)" ) - guard let swiftUrl = toolchain.swift?.asURL else { + guard let swiftUrl = toolchain.swift else { continue } @@ -461,7 +461,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { let requestInfo = requestInfo let executor = OutOfProcessSourceKitRequestExecutor( - sourcekitd: sourcekitd.asURL, + sourcekitd: sourcekitd, swiftFrontend: swiftFrontend, reproducerPredicate: nil ) diff --git a/Sources/Diagnose/IndexCommand.swift b/Sources/Diagnose/IndexCommand.swift index 1c93274d1..849c23c69 100644 --- a/Sources/Diagnose/IndexCommand.swift +++ b/Sources/Diagnose/IndexCommand.swift @@ -105,15 +105,15 @@ package struct IndexCommand: AsyncParsableCommand { ) let installPath = - if let toolchainOverride, let toolchain = Toolchain(try AbsolutePath(validating: toolchainOverride)) { + if let toolchainOverride, let toolchain = Toolchain(URL(fileURLWithPath: toolchainOverride)) { toolchain.path } else { - try AbsolutePath(validating: Bundle.main.bundlePath) + Bundle.main.bundleURL } let messageHandler = IndexLogMessageHandler() let inProcessClient = try await InProcessSourceKitLSPClient( - toolchainPath: installPath?.asURL, + toolchainPath: installPath, options: options, workspaceFolders: [WorkspaceFolder(uri: DocumentURI(URL(fileURLWithPath: project)))], messageHandler: messageHandler diff --git a/Sources/Diagnose/ReduceCommand.swift b/Sources/Diagnose/ReduceCommand.swift index c21278df2..1d09aba7c 100644 --- a/Sources/Diagnose/ReduceCommand.swift +++ b/Sources/Diagnose/ReduceCommand.swift @@ -68,10 +68,9 @@ package struct ReduceCommand: AsyncParsableCommand { var toolchain: Toolchain? { get async throws { if let toolchainOverride { - return Toolchain(try AbsolutePath(validating: toolchainOverride)) + return Toolchain(URL(fileURLWithPath: toolchainOverride)) } - let installPath = try AbsolutePath(validating: Bundle.main.bundlePath) - return await ToolchainRegistry(installPath: installPath).default + return await ToolchainRegistry(installPath: Bundle.main.bundleURL).default } } @@ -92,7 +91,7 @@ package struct ReduceCommand: AsyncParsableCommand { let requestInfo = try RequestInfo(request: request) let executor = OutOfProcessSourceKitRequestExecutor( - sourcekitd: sourcekitd.asURL, + sourcekitd: sourcekitd, swiftFrontend: swiftFrontend, reproducerPredicate: nsPredicate ) diff --git a/Sources/Diagnose/ReduceFrontendCommand.swift b/Sources/Diagnose/ReduceFrontendCommand.swift index 3442d565b..8d8d5b41f 100644 --- a/Sources/Diagnose/ReduceFrontendCommand.swift +++ b/Sources/Diagnose/ReduceFrontendCommand.swift @@ -76,10 +76,9 @@ package struct ReduceFrontendCommand: AsyncParsableCommand { var toolchain: Toolchain? { get async throws { if let toolchainOverride { - return Toolchain(try AbsolutePath(validating: toolchainOverride)) + return Toolchain(URL(fileURLWithPath: toolchainOverride)) } - let installPath = try AbsolutePath(validating: Bundle.main.bundlePath) - return await ToolchainRegistry(installPath: installPath).default + return await ToolchainRegistry(installPath: Bundle.main.bundleURL).default } } @@ -100,7 +99,7 @@ package struct ReduceFrontendCommand: AsyncParsableCommand { ) let executor = OutOfProcessSourceKitRequestExecutor( - sourcekitd: sourcekitd.asURL, + sourcekitd: sourcekitd, swiftFrontend: swiftFrontend, reproducerPredicate: nsPredicate ) diff --git a/Sources/Diagnose/ReproducerBundle.swift b/Sources/Diagnose/ReproducerBundle.swift index d03e868a0..72851aef2 100644 --- a/Sources/Diagnose/ReproducerBundle.swift +++ b/Sources/Diagnose/ReproducerBundle.swift @@ -29,7 +29,7 @@ func makeReproducerBundle(for requestInfo: RequestInfo, toolchain: Toolchain, bu encoding: .utf8 ) if let toolchainPath = toolchain.path { - try toolchainPath.asURL.realpath.filePath + try toolchainPath.realpath.filePath .write( to: bundlePath.appendingPathComponent("toolchain.txt"), atomically: true, diff --git a/Sources/Diagnose/RunSourcekitdRequestCommand.swift b/Sources/Diagnose/RunSourcekitdRequestCommand.swift index ffb47ed89..db383307a 100644 --- a/Sources/Diagnose/RunSourcekitdRequestCommand.swift +++ b/Sources/Diagnose/RunSourcekitdRequestCommand.swift @@ -15,6 +15,7 @@ package import ArgumentParser import Csourcekitd import Foundation import SKUtilities +import SwiftExtensions import SourceKitD import ToolchainRegistry @@ -25,6 +26,7 @@ import Csourcekitd import Foundation import SKUtilities import SourceKitD +import SwiftExtensions import ToolchainRegistry import struct TSCBasic.AbsolutePath @@ -55,18 +57,17 @@ package struct RunSourceKitdRequestCommand: AsyncParsableCommand { package init() {} package func run() async throws { - let installPath = try AbsolutePath(validating: Bundle.main.bundlePath) let sourcekitdPath = if let sourcekitdPath { - sourcekitdPath - } else if let path = await ToolchainRegistry(installPath: installPath).default?.sourcekitd?.pathString { + URL(fileURLWithPath: sourcekitdPath) + } else if let path = await ToolchainRegistry(installPath: Bundle.main.bundleURL).default?.sourcekitd { path } else { print("Did not find sourcekitd in the toolchain. Specify path to sourcekitd manually by passing --sourcekitd") throw ExitCode(1) } let sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate( - dylibPath: try! AbsolutePath(validating: sourcekitdPath) + dylibPath: sourcekitdPath ) var lastResponse: SKDResponse? diff --git a/Sources/Diagnose/Toolchain+SwiftFrontend.swift b/Sources/Diagnose/Toolchain+SwiftFrontend.swift index b7d0ae55d..b3c6fe02f 100644 --- a/Sources/Diagnose/Toolchain+SwiftFrontend.swift +++ b/Sources/Diagnose/Toolchain+SwiftFrontend.swift @@ -27,6 +27,6 @@ extension Toolchain { /// /// - Note: Not discovered as part of the toolchain because `swift-frontend` is only needed in the diagnose commands. package var swiftFrontend: URL? { - return swift?.asURL.deletingLastPathComponent().appendingPathComponent("swift-frontend") + return swift?.deletingLastPathComponent().appendingPathComponent("swift-frontend") } } diff --git a/Sources/InProcessClient/InProcessSourceKitLSPClient.swift b/Sources/InProcessClient/InProcessSourceKitLSPClient.swift index 624a82562..52b6ca34e 100644 --- a/Sources/InProcessClient/InProcessSourceKitLSPClient.swift +++ b/Sources/InProcessClient/InProcessSourceKitLSPClient.swift @@ -70,7 +70,7 @@ public final class InProcessSourceKitLSPClient: Sendable { let serverToClientConnection = LocalConnection(receiverName: "client") self.server = SourceKitLSPServer( client: serverToClientConnection, - toolchainRegistry: ToolchainRegistry(installPath: AbsolutePath(validatingOrNil: try? toolchainPath?.filePath)), + toolchainRegistry: ToolchainRegistry(installPath: toolchainPath), options: options, testHooks: TestHooks(), onExit: { diff --git a/Sources/SKTestSupport/FileSystem.swift b/Sources/SKTestSupport/FileManager+createFiles.swift similarity index 60% rename from Sources/SKTestSupport/FileSystem.swift rename to Sources/SKTestSupport/FileManager+createFiles.swift index c36b33e8f..e919a175e 100644 --- a/Sources/SKTestSupport/FileSystem.swift +++ b/Sources/SKTestSupport/FileManager+createFiles.swift @@ -11,27 +11,22 @@ //===----------------------------------------------------------------------===// #if compiler(>=6) -package import struct TSCBasic.AbsolutePath -package import struct TSCBasic.ByteString -package import protocol TSCBasic.FileSystem +package import Foundation #else -import struct TSCBasic.AbsolutePath -import struct TSCBasic.ByteString -import protocol TSCBasic.FileSystem +import Foundation #endif -extension FileSystem { - +extension FileManager { /// Creates files from a dictionary of path to contents. /// /// - parameters: /// - root: The root directory that the paths are relative to. /// - files: Dictionary from path (relative to root) to contents. - package func createFiles(root: AbsolutePath = .root, files: [String: ByteString]) throws { + package func createFiles(root: URL, files: [String: String]) throws { for (path, contents) in files { - let path = try AbsolutePath(validating: path, relativeTo: root) - try createDirectory(path.parentDirectory, recursive: true) - try writeFileContents(path, bytes: contents) + let path = URL(fileURLWithPath: path, relativeTo: root) + try createDirectory(at: path.deletingLastPathComponent(), withIntermediateDirectories: true) + try contents.write(to: path, atomically: true, encoding: .utf8) } } } diff --git a/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift b/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift index ab1505305..150a34f6e 100644 --- a/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift +++ b/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift @@ -62,7 +62,7 @@ package struct IndexedSingleSwiftFileTestProject { let testFileURL = testWorkspaceDirectory.appendingPathComponent("test.swift") let indexURL = testWorkspaceDirectory.appendingPathComponent("index") self.indexDBURL = testWorkspaceDirectory.appendingPathComponent("index-db") - guard let swiftc = await ToolchainRegistry.forTesting.default?.swiftc?.asURL else { + guard let swiftc = await ToolchainRegistry.forTesting.default?.swiftc else { throw Error.swiftcNotFound } diff --git a/Sources/SKTestSupport/MultiFileTestProject.swift b/Sources/SKTestSupport/MultiFileTestProject.swift index a487c7b8b..1d1d107f3 100644 --- a/Sources/SKTestSupport/MultiFileTestProject.swift +++ b/Sources/SKTestSupport/MultiFileTestProject.swift @@ -104,9 +104,15 @@ package class MultiFileTestProject { var fileData: [String: FileData] = [:] for (fileLocation, markedText) in files { + // Drop trailing slashes from the test dir URL, so tests can write `$TEST_DIR_URL/someFile.swift` without ending + // up with double slashes. + var testDirUrl = scratchDirectory.absoluteString + while testDirUrl.hasSuffix("/") { + testDirUrl = String(testDirUrl.dropLast()) + } let markedText = markedText - .replacingOccurrences(of: "$TEST_DIR_URL", with: scratchDirectory.absoluteString) + .replacingOccurrences(of: "$TEST_DIR_URL", with: testDirUrl) .replacingOccurrences(of: "$TEST_DIR", with: try scratchDirectory.filePath) let fileURL = fileLocation.url(relativeTo: scratchDirectory) try FileManager.default.createDirectory( diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index 53cf2d24c..dbfe49777 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -336,7 +336,7 @@ package actor SkipUnless { } let result = try await Process.run( - arguments: [swift.pathString, "build", "--help-hidden"], + arguments: [swift.filePath, "build", "--help-hidden"], workingDirectory: nil ) guard let output = String(bytes: try result.output.get(), encoding: .utf8) else { @@ -393,7 +393,7 @@ package actor SkipUnless { // of Lib when building Lib. for target in ["MyPlugin", "Lib"] { var arguments = [ - swift.pathString, "build", "--package-path", try project.scratchDirectory.filePath, "--target", target, + try swift.filePath, "build", "--package-path", try project.scratchDirectory.filePath, "--target", target, ] if let globalModuleCache = try globalModuleCache { arguments += ["-Xswiftc", "-module-cache-path", "-Xswiftc", try globalModuleCache.filePath] @@ -474,19 +474,19 @@ package actor SkipUnless { line: UInt = #line ) async throws { return try await shared.skipUnlessSupported(allowSkippingInCI: true, file: file, line: line) { - let swiftFrontend = try await unwrap(ToolchainRegistry.forTesting.default?.swift).parentDirectory - .appending(component: "swift-frontend") + let swiftFrontend = try await unwrap(ToolchainRegistry.forTesting.default?.swift).deletingLastPathComponent() + .appendingPathComponent("swift-frontend") return try await withTestScratchDir { scratchDirectory in - let input = scratchDirectory.appending(component: "Input.swift") - guard FileManager.default.createFile(atPath: input.pathString, contents: nil) else { + let input = scratchDirectory.appendingPathComponent("Input.swift") + guard FileManager.default.createFile(atPath: input.path, contents: nil) else { struct FailedToCrateInputFileError: Error {} throw FailedToCrateInputFileError() } // If we can't compile for wasm, this fails complaining that it can't find the stdlib for wasm. let process = Process( - args: swiftFrontend.pathString, + args: try swiftFrontend.filePath, "-typecheck", - input.pathString, + try input.filePath, "-triple", "wasm32-unknown-none-wasm", "-enable-experimental-feature", diff --git a/Sources/SKTestSupport/SwiftPMTestProject.swift b/Sources/SKTestSupport/SwiftPMTestProject.swift index 5abe722fd..5207eebd1 100644 --- a/Sources/SKTestSupport/SwiftPMTestProject.swift +++ b/Sources/SKTestSupport/SwiftPMTestProject.swift @@ -224,7 +224,7 @@ package class SwiftPMTestProject: MultiFileTestProject { /// Build a SwiftPM package package manifest is located in the directory at `path`. package static func build(at path: URL, extraArguments: [String] = []) async throws { - guard let swift = await ToolchainRegistry.forTesting.default?.swift?.asURL else { + guard let swift = await ToolchainRegistry.forTesting.default?.swift else { throw Error.swiftNotFound } var arguments = @@ -246,7 +246,7 @@ package class SwiftPMTestProject: MultiFileTestProject { /// Resolve package dependencies for the package at `path`. package static func resolvePackageDependencies(at path: URL) async throws { - guard let swift = await ToolchainRegistry.forTesting.default?.swift?.asURL else { + guard let swift = await ToolchainRegistry.forTesting.default?.swift else { throw Error.swiftNotFound } let arguments = [ diff --git a/Sources/SKTestSupport/Utils.swift b/Sources/SKTestSupport/Utils.swift index d7ccc47eb..2d511901d 100644 --- a/Sources/SKTestSupport/Utils.swift +++ b/Sources/SKTestSupport/Utils.swift @@ -14,7 +14,7 @@ package import Foundation package import LanguageServerProtocol import SwiftExtensions -package import struct TSCBasic.AbsolutePath +import struct TSCBasic.AbsolutePath #else import Foundation import LanguageServerProtocol @@ -73,11 +73,11 @@ package func testScratchDir(testName: String = #function) throws -> URL { #if os(Windows) let url = try FileManager.default.temporaryDirectory.realpath .appendingPathComponent("lsp-test") - .appendingPathComponent("\(uuid)") + .appendingPathComponent("\(uuid)", isDirectory: true) #else let url = try FileManager.default.temporaryDirectory.realpath .appendingPathComponent("sourcekit-lsp-test-scratch") - .appendingPathComponent("\(testBaseName)-\(uuid)") + .appendingPathComponent("\(testBaseName)-\(uuid)", isDirectory: true) #endif try? FileManager.default.removeItem(at: url) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) @@ -90,7 +90,7 @@ package func testScratchDir(testName: String = #function) throws -> URL { /// The temporary directory will be deleted at the end of `directory` unless the /// `SOURCEKIT_LSP_KEEP_TEST_SCRATCH_DIR` environment variable is set. package func withTestScratchDir( - @_inheritActorContext _ body: @Sendable (AbsolutePath) async throws -> T, + @_inheritActorContext _ body: @Sendable (URL) async throws -> T, testName: String = #function ) async throws -> T { let scratchDirectory = try testScratchDir(testName: testName) @@ -100,7 +100,7 @@ package func withTestScratchDir( try? FileManager.default.removeItem(at: scratchDirectory) } } - return try await body(try AbsolutePath(validating: scratchDirectory.filePath)) + return try await body(scratchDirectory) } var globalModuleCache: URL? { diff --git a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift index f5f0a21ac..d0cf21e7b 100644 --- a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift +++ b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift @@ -335,7 +335,7 @@ package struct UpdateIndexStoreTaskDescription: IndexTaskDescription { try await runIndexingProcess( indexFile: uri, buildSettings: buildSettings, - processArguments: [swiftc.pathString] + indexingArguments, + processArguments: [swiftc.filePath] + indexingArguments, workingDirectory: buildSettings.workingDirectory.map(AbsolutePath.init(validating:)) ) } @@ -360,7 +360,7 @@ package struct UpdateIndexStoreTaskDescription: IndexTaskDescription { try await runIndexingProcess( indexFile: uri, buildSettings: buildSettings, - processArguments: [clang.pathString] + indexingArguments, + processArguments: [clang.filePath] + indexingArguments, workingDirectory: buildSettings.workingDirectory.map(AbsolutePath.init(validating:)) ) } diff --git a/Sources/SourceKitD/DynamicallyLoadedSourceKitD.swift b/Sources/SourceKitD/DynamicallyLoadedSourceKitD.swift index 62521a72c..865b99eef 100644 --- a/Sources/SourceKitD/DynamicallyLoadedSourceKitD.swift +++ b/Sources/SourceKitD/DynamicallyLoadedSourceKitD.swift @@ -12,18 +12,14 @@ #if compiler(>=6) package import Csourcekitd -import Foundation +package import Foundation import SKLogging import SwiftExtensions - -package import struct TSCBasic.AbsolutePath #else import Csourcekitd import Foundation import SKLogging import SwiftExtensions - -import struct TSCBasic.AbsolutePath #endif extension sourcekitd_api_keys: @unchecked Sendable {} @@ -37,7 +33,7 @@ extension sourcekitd_api_values: @unchecked Sendable {} /// `set_notification_handler`, which are global state managed internally by this class. package actor DynamicallyLoadedSourceKitD: SourceKitD { /// The path to the sourcekitd dylib. - package let path: AbsolutePath + package let path: URL /// The handle to the dylib. let dylib: DLHandle @@ -59,17 +55,17 @@ package actor DynamicallyLoadedSourceKitD: SourceKitD { /// List of notification handlers that will be called for each notification. private var notificationHandlers: [WeakSKDNotificationHandler] = [] - package static func getOrCreate(dylibPath: AbsolutePath) async throws -> SourceKitD { + package static func getOrCreate(dylibPath: URL) async throws -> SourceKitD { try await SourceKitDRegistry.shared .getOrAdd(dylibPath, create: { try DynamicallyLoadedSourceKitD(dylib: dylibPath) }) } - init(dylib path: AbsolutePath) throws { + init(dylib path: URL) throws { self.path = path #if os(Windows) - self.dylib = try dlopen(path.pathString, mode: []) + self.dylib = try dlopen(path.filePath, mode: []) #else - self.dylib = try dlopen(path.pathString, mode: [.lazy, .local, .first]) + self.dylib = try dlopen(path.filePath, mode: [.lazy, .local, .first]) #endif self.api = try sourcekitd_api_functions_t(self.dylib) self.keys = sourcekitd_api_keys(api: self.api) diff --git a/Sources/SourceKitD/SourceKitDRegistry.swift b/Sources/SourceKitD/SourceKitDRegistry.swift index bb0ae7f6c..3b28ace27 100644 --- a/Sources/SourceKitD/SourceKitDRegistry.swift +++ b/Sources/SourceKitD/SourceKitDRegistry.swift @@ -10,12 +10,10 @@ // //===----------------------------------------------------------------------===// -import Foundation - #if compiler(>=6) -package import struct TSCBasic.AbsolutePath +package import Foundation #else -import struct TSCBasic.AbsolutePath +import Foundation #endif /// The set of known SourceKitD instances, uniqued by path. @@ -30,10 +28,10 @@ import struct TSCBasic.AbsolutePath package actor SourceKitDRegistry { /// Mapping from path to active SourceKitD instance. - private var active: [AbsolutePath: SourceKitD] = [:] + private var active: [URL: SourceKitD] = [:] /// Instances that have been unregistered, but may be resurrected if accessed before destruction. - private var cemetary: [AbsolutePath: WeakSourceKitD] = [:] + private var cemetary: [URL: WeakSourceKitD] = [:] /// Initialize an empty registry. package init() {} @@ -43,7 +41,7 @@ package actor SourceKitDRegistry { /// Returns the existing SourceKitD for the given path, or creates it and registers it. package func getOrAdd( - _ key: AbsolutePath, + _ key: URL, create: @Sendable () throws -> SourceKitD ) rethrows -> SourceKitD { if let existing = active[key] { @@ -66,7 +64,7 @@ package actor SourceKitDRegistry { /// is converted to a weak reference until it is no longer referenced anywhere by the program. If /// the same path is looked up again before the original service is deinitialized, the original /// service is resurrected rather than creating a new instance. - package func remove(_ key: AbsolutePath) -> SourceKitD? { + package func remove(_ key: URL) -> SourceKitD? { let existing = active.removeValue(forKey: key) if let existing = existing { assert(self.cemetary[key]?.value == nil) diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift index d3465072c..34f744f90 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift @@ -22,8 +22,6 @@ import SwiftSyntax import TSCExtensions import ToolchainRegistry -import struct TSCBasic.AbsolutePath - #if os(Windows) import WinSDK #endif @@ -59,10 +57,10 @@ actor ClangLanguageService: LanguageService, MessageHandler { var capabilities: ServerCapabilities? = nil /// Path to the clang binary. - let clangPath: AbsolutePath? + let clangPath: URL? /// Path to the `clangd` binary. - let clangdPath: AbsolutePath + let clangdPath: URL let clangdOptions: [String] @@ -178,7 +176,7 @@ actor ClangLanguageService: LanguageService, MessageHandler { openDocuments = [:] let (connectionToClangd, process) = try JSONRPCConnection.start( - executable: clangdPath.asURL, + executable: clangdPath, arguments: [ "-compile_args_from=lsp", // Provide compiler args programmatically. "-background-index=false", // Disable clangd indexing, we use the build @@ -459,9 +457,7 @@ extension ClangLanguageService { let clangBuildSettings = await self.buildSettings(for: uri, fallbackAfterTimeout: false) // The compile command changed, send over the new one. - if let compileCommand = clangBuildSettings?.compileCommand, - let pathString = AbsolutePath(validatingOrNil: try? url.filePath)?.pathString - { + if let compileCommand = clangBuildSettings?.compileCommand, let pathString = try? url.filePath { let notification = DidChangeConfigurationNotification( settings: .clangd(ClangWorkspaceSettings(compilationDatabaseChanges: [pathString: compileCommand])) ) @@ -644,8 +640,8 @@ private struct ClangBuildSettings: Equatable { /// fallback arguments and represent the file state differently. package let isFallback: Bool - package init(_ settings: FileBuildSettings, clangPath: AbsolutePath?) { - var arguments = [clangPath?.pathString ?? "clang"] + settings.compilerArguments + package init(_ settings: FileBuildSettings, clangPath: URL?) { + var arguments = [(try? clangPath?.filePath) ?? "clang"] + settings.compilerArguments if arguments.contains("-fmodules") { // Clangd is not built with support for the 'obj' format. arguments.append(contentsOf: [ diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 8bb971d9d..6cedb6c50 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -30,7 +30,6 @@ package import ToolchainRegistry import struct PackageModel.BuildFlags import struct TSCBasic.AbsolutePath import protocol TSCBasic.FileSystem -import var TSCBasic.localFileSystem #else import BuildServerProtocol import BuildSystemIntegration @@ -51,7 +50,6 @@ import ToolchainRegistry import struct PackageModel.BuildFlags import struct TSCBasic.AbsolutePath import protocol TSCBasic.FileSystem -import var TSCBasic.localFileSystem #endif /// Disambiguate LanguageServerProtocol.Language and IndexstoreDB.Language @@ -540,7 +538,7 @@ package actor SourceKitLSPServer { logger.log( """ - Using toolchain at \(toolchain.path?.pathString ?? "") (\(toolchain.identifier, privacy: .public)) \ + Using toolchain at \(toolchain.path?.description ?? "") (\(toolchain.identifier, privacy: .public)) \ for \(uri.forLogging) """ ) diff --git a/Sources/SourceKitLSP/Swift/DocumentFormatting.swift b/Sources/SourceKitLSP/Swift/DocumentFormatting.swift index 9f305c1fa..82e6c6a61 100644 --- a/Sources/SourceKitLSP/Swift/DocumentFormatting.swift +++ b/Sources/SourceKitLSP/Swift/DocumentFormatting.swift @@ -15,6 +15,7 @@ import Foundation package import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging +import SwiftExtensions import SwiftParser import SwiftSyntax import TSCExtensions @@ -27,6 +28,7 @@ import Foundation import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging +import SwiftExtensions import SwiftParser import SwiftSyntax import TSCExtensions @@ -168,7 +170,7 @@ extension SwiftLanguageService { } var args = try [ - swiftFormat.pathString, + swiftFormat.filePath, "format", "--configuration", swiftFormatConfiguration(for: textDocument.uri, options: options), diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift index da1f2bb83..ca83985b7 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift @@ -28,8 +28,6 @@ import SwiftParser import SwiftParserDiagnostics package import SwiftSyntax package import ToolchainRegistry - -import struct TSCBasic.AbsolutePath #else import BuildSystemIntegration import Csourcekitd @@ -48,8 +46,6 @@ import SwiftParser import SwiftParserDiagnostics import SwiftSyntax import ToolchainRegistry - -import struct TSCBasic.AbsolutePath #endif #if os(Windows) @@ -125,7 +121,7 @@ package actor SwiftLanguageService: LanguageService, Sendable { let sourcekitd: SourceKitD /// Path to the swift-format executable if it exists in the toolchain. - let swiftFormat: AbsolutePath? + let swiftFormat: URL? /// Queue on which notifications from sourcekitd are handled to ensure we are /// handling them in-order. diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index cebfd34ed..36f7cd78d 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -232,7 +232,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { ) logger.log( - "Created workspace at \(rootUri.forLogging) with project root \(buildSystemSpec?.projectRoot.pathString ?? "")" + "Created workspace at \(rootUri.forLogging) with project root \(buildSystemSpec?.projectRoot.description ?? "")" ) var index: IndexStoreDB? = nil @@ -249,7 +249,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { ) if let indexStorePath, let indexDatabasePath, let libPath = await toolchainRegistry.default?.libIndexStore { do { - let lib = try IndexStoreLibrary(dylibPath: libPath.pathString) + let lib = try IndexStoreLibrary(dylibPath: libPath.filePath) indexDelegate = SourceKitIndexDelegate() let prefixMappings = indexOptions.indexPrefixMap?.map { PathPrefixMapping(original: $0.key, replacement: $0.value) } ?? [] diff --git a/Sources/SwiftExtensions/FileManagerExtensions.swift b/Sources/SwiftExtensions/FileManagerExtensions.swift index 9b8979302..0167021a6 100644 --- a/Sources/SwiftExtensions/FileManagerExtensions.swift +++ b/Sources/SwiftExtensions/FileManagerExtensions.swift @@ -24,4 +24,17 @@ extension FileManager { } return self.fileExists(atPath: filePath) } + + /// Returns `true` if an entry exists in the file system at the given URL and that entry is a directory. + package func isDirectory(at url: URL) -> Bool { + var isDirectory: ObjCBool = false + return self.fileExists(atPath: url.path, isDirectory: &isDirectory) && isDirectory.boolValue + } + + /// Returns `true` if an entry exists in the file system at the given URL and that entry is a file, ie. not a + /// directory. + package func isFile(at url: URL) -> Bool { + var isDirectory: ObjCBool = false + return self.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue + } } diff --git a/Sources/SwiftExtensions/URLExtensions.swift b/Sources/SwiftExtensions/URLExtensions.swift index 10c8501f1..024e91f6f 100644 --- a/Sources/SwiftExtensions/URLExtensions.swift +++ b/Sources/SwiftExtensions/URLExtensions.swift @@ -76,9 +76,19 @@ extension URL { } } - /// Deprecate `path` to encourage using `filePath`, at least if `SwiftExtensions` is imported. - @available(*, deprecated, message: "Use filePath instead", renamed: "filePath") - package var path: String { - return try! filePath + package var isRoot: Bool { + #if os(Windows) + // FIXME: We should call into Windows' native check to check if this path is a root once https://github.com/swiftlang/swift-foundation/issues/976 is fixed. + return self.pathComponents.count <= 1 + #else + // On Linux, we may end up with an string for the path due to https://github.com/swiftlang/swift-foundation/issues/980 + // TODO: Remove the check for "" once https://github.com/swiftlang/swift-foundation/issues/980 is fixed. + return self.path == "/" || self.path == "" + #endif + } + + /// Returns true if the path of `self` starts with the path in `other`. + package func isDescendant(of other: URL) -> Bool { + return self.pathComponents.dropLast().starts(with: other.pathComponents) } } diff --git a/Sources/TSCExtensions/CMakeLists.txt b/Sources/TSCExtensions/CMakeLists.txt index 7cc1a57d1..e4b507970 100644 --- a/Sources/TSCExtensions/CMakeLists.txt +++ b/Sources/TSCExtensions/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(TSCExtensions STATIC ByteString.swift Process+Run.swift SwitchableProcessResultExitStatus.swift + URL+appendingRelativePath.swift ) set_target_properties(TSCExtensions PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/TSCExtensions/URL+appendingRelativePath.swift b/Sources/TSCExtensions/URL+appendingRelativePath.swift new file mode 100644 index 000000000..28d3395d0 --- /dev/null +++ b/Sources/TSCExtensions/URL+appendingRelativePath.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2020 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 SwiftExtensions + +#if compiler(>=6) +package import struct TSCBasic.AbsolutePath +package import struct TSCBasic.RelativePath +package import Foundation +#else +import struct TSCBasic.AbsolutePath +import struct TSCBasic.RelativePath +import Foundation +#endif + +extension URL { + package func appending(_ relativePath: RelativePath) -> URL { + var result = self + for component in relativePath.components { + if component == "." { + continue + } + result.appendPathComponent(component) + } + return result + } +} diff --git a/Sources/ToolchainRegistry/Toolchain.swift b/Sources/ToolchainRegistry/Toolchain.swift index cfd77a326..e06a63cf6 100644 --- a/Sources/ToolchainRegistry/Toolchain.swift +++ b/Sources/ToolchainRegistry/Toolchain.swift @@ -10,23 +10,17 @@ // //===----------------------------------------------------------------------===// -import Foundation import RegexBuilder import SKLogging import SwiftExtensions -#if compiler(>=6) import enum PackageLoading.Platform -package import struct TSCBasic.AbsolutePath -package import protocol TSCBasic.FileSystem -package import class TSCBasic.Process -package import var TSCBasic.localFileSystem -#else -import enum PackageLoading.Platform -import struct TSCBasic.AbsolutePath -import protocol TSCBasic.FileSystem import class TSCBasic.Process -import var TSCBasic.localFileSystem + +#if compiler(>=6) +package import Foundation +#else +import Foundation #endif /// A Swift version consisting of the major and minor component. @@ -84,30 +78,30 @@ public final class Toolchain: Sendable { /// The path to this toolchain, if applicable. /// /// For example, this may be the path to an ".xctoolchain" directory. - package let path: AbsolutePath? + package let path: URL? // MARK: Tool Paths /// The path to the Clang compiler if available. - package let clang: AbsolutePath? + package let clang: URL? /// The path to the Swift driver if available. - package let swift: AbsolutePath? + package let swift: URL? /// The path to the Swift compiler if available. - package let swiftc: AbsolutePath? + package let swiftc: URL? /// The path to the swift-format executable, if available. - package let swiftFormat: AbsolutePath? + package let swiftFormat: URL? /// The path to the clangd language server if available. - package let clangd: AbsolutePath? + package let clangd: URL? /// The path to the Swift language server if available. - package let sourcekitd: AbsolutePath? + package let sourcekitd: URL? /// The path to the indexstore library if available. - package let libIndexStore: AbsolutePath? + package let libIndexStore: URL? private let swiftVersionTask = ThreadSafeBox?>(initialValue: nil) @@ -124,7 +118,7 @@ public final class Toolchain: Sendable { throw SwiftVersionParsingError.failedToFindSwiftc } - let process = Process(args: swiftc.pathString, "--version") + let process = Process(args: try swiftc.filePath, "--version") try process.launch() let result = try await process.waitUntilExit() let output = String(bytes: try result.output.get(), encoding: .utf8) @@ -153,14 +147,14 @@ public final class Toolchain: Sendable { package init( identifier: String, displayName: String, - path: AbsolutePath? = nil, - clang: AbsolutePath? = nil, - swift: AbsolutePath? = nil, - swiftc: AbsolutePath? = nil, - swiftFormat: AbsolutePath? = nil, - clangd: AbsolutePath? = nil, - sourcekitd: AbsolutePath? = nil, - libIndexStore: AbsolutePath? = nil + path: URL? = nil, + clang: URL? = nil, + swift: URL? = nil, + swiftc: URL? = nil, + swiftFormat: URL? = nil, + clangd: URL? = nil, + sourcekitd: URL? = nil, + libIndexStore: URL? = nil ) { self.identifier = identifier self.displayName = displayName @@ -182,7 +176,7 @@ public final class Toolchain: Sendable { /// `libIndexStore`. These toolchains are not comparable. /// - Two toolchains that both contain `swiftc` and `clangd` are supersets of each other. func isSuperset(of other: Toolchain) -> Bool { - func isSuperset(for tool: KeyPath) -> Bool { + func isSuperset(for tool: KeyPath) -> Bool { if self[keyPath: tool] == nil && other[keyPath: tool] != nil { // This toolchain doesn't contain the tool but the other toolchain does. It is not a superset. return false @@ -221,65 +215,69 @@ extension Toolchain { /// /// If `path` contains an ".xctoolchain", we try to read an Info.plist file to provide the /// toolchain identifier, etc. Otherwise this information is derived from the path. - convenience package init?(_ path: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) { + convenience package init?(_ path: URL) { // Properties that need to be initialized let identifier: String let displayName: String - let toolchainPath: AbsolutePath? - var clang: AbsolutePath? = nil - var clangd: AbsolutePath? = nil - var swift: AbsolutePath? = nil - var swiftc: AbsolutePath? = nil - var swiftFormat: AbsolutePath? = nil - var sourcekitd: AbsolutePath? = nil - var libIndexStore: AbsolutePath? = nil - - if let (infoPlist, xctoolchainPath) = containingXCToolchain(path, fileSystem) { + let toolchainPath: URL? + var clang: URL? = nil + var clangd: URL? = nil + var swift: URL? = nil + var swiftc: URL? = nil + var swiftFormat: URL? = nil + var sourcekitd: URL? = nil + var libIndexStore: URL? = nil + + if let (infoPlist, xctoolchainPath) = containingXCToolchain(path) { identifier = infoPlist.identifier - displayName = infoPlist.displayName ?? xctoolchainPath.basenameWithoutExt + displayName = infoPlist.displayName ?? xctoolchainPath.deletingPathExtension().lastPathComponent toolchainPath = xctoolchainPath } else { - identifier = path.pathString - displayName = path.basename + identifier = (try? path.filePath) ?? path.path + displayName = path.lastPathComponent toolchainPath = path } // Find tools in the toolchain var foundAny = false - let searchPaths = [path, path.appending(components: "bin"), path.appending(components: "usr", "bin")] + let searchPaths = [ + path, path.appendingPathComponent("bin"), path.appendingPathComponent("usr").appendingPathComponent("bin"), + ] for binPath in searchPaths { - let libPath = binPath.parentDirectory.appending(component: "lib") + let libPath = binPath.deletingLastPathComponent().appendingPathComponent("lib") - guard fileSystem.isDirectory(binPath) || fileSystem.isDirectory(libPath) else { continue } + guard FileManager.default.isDirectory(at: binPath) || FileManager.default.isDirectory(at: libPath) else { + continue + } let execExt = Platform.current?.executableExtension ?? "" - let clangPath = binPath.appending(component: "clang\(execExt)") - if fileSystem.isExecutableFile(clangPath) { + let clangPath = binPath.appendingPathComponent("clang\(execExt)") + if FileManager.default.isExecutableFile(atPath: clangPath.path) { clang = clangPath foundAny = true } - let clangdPath = binPath.appending(component: "clangd\(execExt)") - if fileSystem.isExecutableFile(clangdPath) { + let clangdPath = binPath.appendingPathComponent("clangd\(execExt)") + if FileManager.default.isExecutableFile(atPath: clangdPath.path) { clangd = clangdPath foundAny = true } - let swiftPath = binPath.appending(component: "swift\(execExt)") - if fileSystem.isExecutableFile(swiftPath) { + let swiftPath = binPath.appendingPathComponent("swift\(execExt)") + if FileManager.default.isExecutableFile(atPath: swiftPath.path) { swift = swiftPath foundAny = true } - let swiftcPath = binPath.appending(component: "swiftc\(execExt)") - if fileSystem.isExecutableFile(swiftcPath) { + let swiftcPath = binPath.appendingPathComponent("swiftc\(execExt)") + if FileManager.default.isExecutableFile(atPath: swiftcPath.path) { swiftc = swiftcPath foundAny = true } - let swiftFormatPath = binPath.appending(component: "swift-format\(execExt)") - if fileSystem.isExecutableFile(swiftFormatPath) { + let swiftFormatPath = binPath.appendingPathComponent("swift-format\(execExt)") + if FileManager.default.isExecutableFile(atPath: swiftFormatPath.path) { swiftFormat = swiftFormatPath foundAny = true } @@ -293,28 +291,28 @@ extension Toolchain { dylibExt = ".so" } - let sourcekitdPath = libPath.appending(components: "sourcekitd.framework", "sourcekitd") - if fileSystem.isFile(sourcekitdPath) { + let sourcekitdPath = libPath.appendingPathComponent("sourcekitd.framework").appendingPathComponent("sourcekitd") + if FileManager.default.isFile(at: sourcekitdPath) { sourcekitd = sourcekitdPath foundAny = true } else { #if os(Windows) - let sourcekitdPath = binPath.appending(component: "sourcekitdInProc\(dylibExt)") + let sourcekitdPath = binPath.appendingPathComponent("sourcekitdInProc\(dylibExt)") #else - let sourcekitdPath = libPath.appending(component: "libsourcekitdInProc\(dylibExt)") + let sourcekitdPath = libPath.appendingPathComponent("libsourcekitdInProc\(dylibExt)") #endif - if fileSystem.isFile(sourcekitdPath) { + if FileManager.default.isFile(at: sourcekitdPath) { sourcekitd = sourcekitdPath foundAny = true } } #if os(Windows) - let libIndexStorePath = binPath.appending(components: "libIndexStore\(dylibExt)") + let libIndexStorePath = binPath.appendingPathComponent("libIndexStore\(dylibExt)") #else - let libIndexStorePath = libPath.appending(components: "libIndexStore\(dylibExt)") + let libIndexStorePath = libPath.appendingPathComponent("libIndexStore\(dylibExt)") #endif - if fileSystem.isFile(libIndexStorePath) { + if FileManager.default.isFile(at: libIndexStorePath) { libIndexStore = libIndexStorePath foundAny = true } @@ -344,18 +342,17 @@ extension Toolchain { /// Find a containing xctoolchain with plist, if available. func containingXCToolchain( - _ path: AbsolutePath, - _ fileSystem: FileSystem -) -> (XCToolchainPlist, AbsolutePath)? { + _ path: URL +) -> (XCToolchainPlist, URL)? { var path = path while !path.isRoot { - if path.extension == "xctoolchain" { - if let infoPlist = orLog("", { try XCToolchainPlist(fromDirectory: path, fileSystem) }) { + if path.pathExtension == "xctoolchain" { + if let infoPlist = orLog("", { try XCToolchainPlist(fromDirectory: path) }) { return (infoPlist, path) } return nil } - path = path.parentDirectory + path = path.deletingLastPathComponent() } return nil } diff --git a/Sources/ToolchainRegistry/ToolchainRegistry.swift b/Sources/ToolchainRegistry/ToolchainRegistry.swift index 2c55162ea..c76d641a1 100644 --- a/Sources/ToolchainRegistry/ToolchainRegistry.swift +++ b/Sources/ToolchainRegistry/ToolchainRegistry.swift @@ -11,26 +11,23 @@ //===----------------------------------------------------------------------===// import Dispatch -import Foundation import LanguageServerProtocolExtensions +import SwiftExtensions import TSCExtensions #if compiler(>=6) -package import struct TSCBasic.AbsolutePath -package import protocol TSCBasic.FileSystem +package import Foundation package import class TSCBasic.Process package import enum TSCBasic.ProcessEnv package import struct TSCBasic.ProcessEnvironmentKey package import func TSCBasic.getEnvSearchPaths -package import var TSCBasic.localFileSystem #else +import Foundation import struct TSCBasic.AbsolutePath -import protocol TSCBasic.FileSystem import class TSCBasic.Process import enum TSCBasic.ProcessEnv import struct TSCBasic.ProcessEnvironmentKey import func TSCBasic.getEnvSearchPaths -import var TSCBasic.localFileSystem #endif /// Set of known toolchains. @@ -74,7 +71,7 @@ package final actor ToolchainRegistry { /// The toolchains indexed by their path. /// /// Note: Not all toolchains have a path. - private let toolchainsByPath: [AbsolutePath: Toolchain] + private let toolchainsByPath: [URL: Toolchain] /// The currently selected toolchain identifier on Darwin. package let darwinToolchainOverride: String? @@ -101,7 +98,7 @@ package final actor ToolchainRegistry { ) { var toolchainsAndReasons: [(toolchain: Toolchain, reason: ToolchainRegisterReason)] = [] var toolchainsByIdentifier: [String: [Toolchain]] = [:] - var toolchainsByPath: [AbsolutePath: Toolchain] = [:] + var toolchainsByPath: [URL: Toolchain] = [:] for (toolchain, reason) in toolchainsAndReasonsParam { // Non-XcodeDefault toolchain: disallow all duplicates. if toolchain.identifier != ToolchainRegistry.darwinDefaultToolchainIdentifier { @@ -137,7 +134,7 @@ package final actor ToolchainRegistry { /// installations but not next to the `sourcekit-lsp` binary because there is no `sourcekit-lsp` binary during /// testing. package static var forTesting: ToolchainRegistry { - ToolchainRegistry(localFileSystem) + ToolchainRegistry() } /// Creates a toolchain registry populated by scanning for toolchains according to the given paths @@ -150,19 +147,20 @@ package final actor ToolchainRegistry { /// * (Darwin) `[~]/Library/Developer/Toolchains` /// * `env SOURCEKIT_PATH, PATH` package init( - installPath: AbsolutePath? = nil, + installPath: URL? = nil, environmentVariables: [ProcessEnvironmentKey] = ["SOURCEKIT_TOOLCHAIN_PATH"], - xcodes: [AbsolutePath] = [_currentXcodeDeveloperPath].compactMap({ $0 }), - darwinToolchainOverride: String? = ProcessEnv.block["TOOLCHAINS"], - _ fileSystem: FileSystem = localFileSystem + xcodes: [URL] = [_currentXcodeDeveloperPath].compactMap({ $0 }), + libraryDirectories: [URL] = FileManager.default.urls(for: .libraryDirectory, in: .allDomainsMask), + pathEnvironmentVariables: [ProcessEnvironmentKey] = ["SOURCEKIT_PATH", "PATH"], + darwinToolchainOverride: String? = ProcessEnv.block["TOOLCHAINS"] ) { // The paths at which we have found toolchains - var toolchainPaths: [(path: AbsolutePath, reason: ToolchainRegisterReason)] = [] + var toolchainPaths: [(path: URL, reason: ToolchainRegisterReason)] = [] // Scan for toolchains in the paths given by `environmentVariables`. for envVar in environmentVariables { - if let pathStr = ProcessEnv.block[envVar], let path = try? AbsolutePath(validating: pathStr) { - toolchainPaths.append((path, .sourcekitToolchainEnvironmentVariable)) + if let pathStr = ProcessEnv.block[envVar] { + toolchainPaths.append((URL(fileURLWithPath: pathStr), .sourcekitToolchainEnvironmentVariable)) } } @@ -172,39 +170,39 @@ package final actor ToolchainRegistry { } // Search for toolchains in the Xcode developer directories and global toolchain install paths - let toolchainSearchPaths = + var toolchainSearchPaths = xcodes.map { - if $0.extension == "app" { - return $0.appending(components: "Contents", "Developer", "Toolchains") + if $0.pathExtension == "app" { + return $0.appendingPathComponent("Contents").appendingPathComponent("Developer").appendingPathComponent( + "Toolchains" + ) } else { - return $0.appending(component: "Toolchains") + return $0.appendingPathComponent("Toolchains") } } - + FileManager.default.urls(for: .libraryDirectory, in: .allDomainsMask).compactMap { - AbsolutePath(validatingOrNil: $0.appendingPathComponent("Developer").appendingPathComponent("Toolchains").path) - } + toolchainSearchPaths += libraryDirectories.compactMap { + $0.appendingPathComponent("Developer").appendingPathComponent("Toolchains") + } for xctoolchainSearchPath in toolchainSearchPaths { - guard let direntries = try? fileSystem.getDirectoryContents(xctoolchainSearchPath) else { - continue - } - for name in direntries { - let path = xctoolchainSearchPath.appending(component: name) - if path.extension == "xctoolchain" { - toolchainPaths.append((path, .xcode)) + let entries = + (try? FileManager.default.contentsOfDirectory(at: xctoolchainSearchPath, includingPropertiesForKeys: nil)) ?? [] + for entry in entries { + if entry.pathExtension == "xctoolchain" { + toolchainPaths.append((entry, .xcode)) } } } // Scan for toolchains by the given PATH-like environment variables. - for envVar: ProcessEnvironmentKey in ["SOURCEKIT_PATH", "PATH", "Path"] { + for envVar: ProcessEnvironmentKey in pathEnvironmentVariables { for path in getEnvSearchPaths(pathString: ProcessEnv.block[envVar], currentWorkingDirectory: nil) { - toolchainPaths.append((path, .pathEnvironmentVariable)) + toolchainPaths.append((path.asURL, .pathEnvironmentVariable)) } } let toolchainsAndReasons = toolchainPaths.compactMap { - if let toolchain = Toolchain($0.path, fileSystem) { + if let toolchain = Toolchain($0.path) { return (toolchain, $0.reason) } return nil @@ -253,7 +251,7 @@ package final actor ToolchainRegistry { } /// Returns the preferred toolchain that contains all the tools at the given key paths. - package func preferredToolchain(containing requiredTools: [KeyPath]) -> Toolchain? { + package func preferredToolchain(containing requiredTools: [KeyPath]) -> Toolchain? { if let toolchain = self.default, requiredTools.allSatisfy({ toolchain[keyPath: $0] != nil }) { return toolchain } @@ -274,15 +272,15 @@ extension ToolchainRegistry { return toolchainsByIdentifier[identifier] ?? [] } - package func toolchain(withPath path: AbsolutePath) -> Toolchain? { + package func toolchain(withPath path: URL) -> Toolchain? { return toolchainsByPath[path] } } extension ToolchainRegistry { /// The path of the current Xcode.app/Contents/Developer. - package static var _currentXcodeDeveloperPath: AbsolutePath? { + package static var _currentXcodeDeveloperPath: URL? { guard let str = try? Process.checkNonZeroExit(args: "/usr/bin/xcode-select", "-p") else { return nil } - return try? AbsolutePath(validating: str.trimmingCharacters(in: .whitespacesAndNewlines)) + return URL(fileURLWithPath: str.trimmingCharacters(in: .whitespacesAndNewlines)) } } diff --git a/Sources/ToolchainRegistry/XCToolchainPlist.swift b/Sources/ToolchainRegistry/XCToolchainPlist.swift index 8d712a74b..716a62fc8 100644 --- a/Sources/ToolchainRegistry/XCToolchainPlist.swift +++ b/Sources/ToolchainRegistry/XCToolchainPlist.swift @@ -12,15 +12,11 @@ import Foundation import LanguageServerProtocolExtensions +import SwiftExtensions import TSCExtensions -import struct TSCBasic.AbsolutePath -import protocol TSCBasic.FileSystem -import var TSCBasic.localFileSystem - #if os(macOS) import struct TSCBasic.RelativePath -import struct TSCBasic.FileSystemError #endif /// A helper type for decoding the Info.plist or ToolchainInfo.plist file from an .xctoolchain. @@ -39,9 +35,9 @@ package struct XCToolchainPlist { } extension XCToolchainPlist { - enum Error: Swift.Error { case unsupportedPlatform + case notFound(missingPlistPath: URL?) } /// Returns the plist contents from the xctoolchain in the given directory, either Info.plist or @@ -49,24 +45,24 @@ extension XCToolchainPlist { /// /// - parameter path: The directory to search. /// - throws: If there is not plist file or it cannot be read. - init(fromDirectory path: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { + init(fromDirectory path: URL) throws { #if os(macOS) let plistNames = [ try RelativePath(validating: "ToolchainInfo.plist"), // Xcode try RelativePath(validating: "Info.plist"), // Swift.org ] - var missingPlistPath: AbsolutePath? + var missingPlistPath: URL? for plistPath in plistNames.lazy.map({ path.appending($0) }) { - if fileSystem.isFile(plistPath) { - try self.init(path: plistPath, fileSystem) + if FileManager.default.isFile(at: plistPath) { + try self.init(path: plistPath) return } missingPlistPath = plistPath } - throw FileSystemError(.noEntry, missingPlistPath) + throw Error.notFound(missingPlistPath: missingPlistPath) #else throw Error.unsupportedPlatform #endif @@ -75,14 +71,12 @@ extension XCToolchainPlist { /// Returns the plist contents from the xctoolchain at `path`. /// /// - parameter path: The directory to search. - init(path: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { + init(path: URL) throws { #if os(macOS) - let bytes = try fileSystem.readFileContents(path) - self = try bytes.withUnsafeData { data in - let decoder = PropertyListDecoder() - var format = PropertyListSerialization.PropertyListFormat.binary - return try decoder.decode(XCToolchainPlist.self, from: data, format: &format) - } + let data = try Data(contentsOf: path) + let decoder = PropertyListDecoder() + var format = PropertyListSerialization.PropertyListFormat.binary + self = try decoder.decode(XCToolchainPlist.self, from: data, format: &format) #else throw Error.unsupportedPlatform #endif diff --git a/Sources/sourcekit-lsp/SourceKitLSP.swift b/Sources/sourcekit-lsp/SourceKitLSP.swift index e3e1d2d9a..c8c0fc0b7 100644 --- a/Sources/sourcekit-lsp/SourceKitLSP.swift +++ b/Sources/sourcekit-lsp/SourceKitLSP.swift @@ -29,10 +29,6 @@ import ToolchainRegistry #if canImport(Android) import Android #endif - -public import struct TSCBasic.AbsolutePath -public import struct TSCBasic.RelativePath -public import var TSCBasic.localFileSystem #else import ArgumentParser import BuildSystemIntegration @@ -48,56 +44,6 @@ import SKOptions import SourceKitLSP import SwiftExtensions import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import struct TSCBasic.RelativePath -import var TSCBasic.localFileSystem -#endif - -extension AbsolutePath { - public init?(argument: String) { - let path: AbsolutePath? - - if let cwd: AbsolutePath = localFileSystem.currentWorkingDirectory { - path = try? AbsolutePath(validating: argument, relativeTo: cwd) - } else { - path = try? AbsolutePath(validating: argument) - } - - guard let path = path else { - return nil - } - - self = path - } - - public static var defaultCompletionKind: CompletionKind { - // This type is most commonly used to select a directory, not a file. - // Specify '.file()' in an argument declaration when necessary. - .directory - } -} -#if compiler(<5.11) -extension AbsolutePath: ExpressibleByArgument {} -#else -extension AbsolutePath: @retroactive ExpressibleByArgument {} -#endif - -extension RelativePath { - public init?(argument: String) { - let path = try? RelativePath(validating: argument) - - guard let path = path else { - return nil - } - - self = path - } -} -#if compiler(<5.11) -extension RelativePath: ExpressibleByArgument {} -#else -extension RelativePath: @retroactive ExpressibleByArgument {} #endif extension PathPrefixMapping { @@ -333,8 +279,6 @@ struct SourceKitLSP: AsyncParsableCommand { outFD: realStdoutHandle ) - let installPath = try AbsolutePath(validating: Bundle.main.bundlePath) - var inputMirror: FileHandle? = nil if let inputMirrorDirectory = globalConfigurationOptions.loggingOrDefault.inputMirrorDirectory { orLog("Setting up input mirror") { @@ -355,7 +299,7 @@ struct SourceKitLSP: AsyncParsableCommand { let server = SourceKitLSPServer( client: clientConnection, - toolchainRegistry: ToolchainRegistry(installPath: installPath, localFileSystem), + toolchainRegistry: ToolchainRegistry(installPath: Bundle.main.bundleURL), options: globalConfigurationOptions, testHooks: TestHooks(), onExit: { diff --git a/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift b/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift index 2a04d452d..c71f90c9c 100644 --- a/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift +++ b/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift @@ -103,7 +103,7 @@ final class BuildSystemManagerTests: XCTestCase { let a = try DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider([a: [a]]) let bsm = await BuildSystemManager( - buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: try AbsolutePath(validating: "/")), + buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: URL(fileURLWithPath: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), connectionToClient: DummyBuildSystemManagerConnectionToClient(), @@ -136,7 +136,7 @@ final class BuildSystemManagerTests: XCTestCase { let a = try DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider([a: [a]]) let bsm = await BuildSystemManager( - buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: try AbsolutePath(validating: "/")), + buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: URL(fileURLWithPath: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), connectionToClient: DummyBuildSystemManagerConnectionToClient(), @@ -158,7 +158,7 @@ final class BuildSystemManagerTests: XCTestCase { let a = try DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider([a: [a]]) let bsm = await BuildSystemManager( - buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: try AbsolutePath(validating: "/")), + buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: URL(fileURLWithPath: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), connectionToClient: DummyBuildSystemManagerConnectionToClient(), @@ -202,7 +202,7 @@ final class BuildSystemManagerTests: XCTestCase { ) let bsm = await BuildSystemManager( - buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: try AbsolutePath(validating: "/")), + buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: URL(fileURLWithPath: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), connectionToClient: DummyBuildSystemManagerConnectionToClient(), @@ -266,7 +266,7 @@ final class BuildSystemManagerTests: XCTestCase { ) let bsm = await BuildSystemManager( - buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: try AbsolutePath(validating: "/")), + buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: URL(fileURLWithPath: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), connectionToClient: DummyBuildSystemManagerConnectionToClient(), diff --git a/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift b/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift index 8891ab238..6c136f947 100644 --- a/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift +++ b/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift @@ -16,9 +16,11 @@ import LanguageServerProtocol import LanguageServerProtocolExtensions import SKTestSupport import SwiftExtensions -import TSCBasic +import TSCExtensions import XCTest +import struct TSCBasic.RelativePath + final class CompilationDatabaseTests: XCTestCase { func testEncodeCompDBCommand() throws { // Requires JSONEncoder.OutputFormatting.sortedKeys @@ -176,115 +178,91 @@ final class CompilationDatabaseTests: XCTestCase { XCTAssertEqual(db[DocumentURI(filePath: "\(fileSystemRoot)b", isDirectory: false)], [cmd3]) } - func testJSONCompilationDatabaseFromDirectory() throws { - let fs = InMemoryFileSystem() - try fs.createDirectory(AbsolutePath(validating: "/a")) - XCTAssertNil( - try tryLoadCompilationDatabase( - directory: AbsolutePath(validating: "/a"), - fs - ) - ) + func testJSONCompilationDatabaseFromDirectory() async throws { + try await withTestScratchDir { tempDir in + XCTAssertNil(tryLoadCompilationDatabase(directory: tempDir)) - try fs.writeFileContents( - AbsolutePath(validating: "/a/compile_commands.json"), - bytes: """ - [ - { - "file": "/a/a.swift", - "directory": "/a", - "arguments": ["swiftc", "/a/a.swift"] - } - ] - """ - ) + try """ + [ + { + "file": "/a/a.swift", + "directory": "/a", + "arguments": ["swiftc", "/a/a.swift"] + } + ] + """.write(to: tempDir.appendingPathComponent("compile_commands.json"), atomically: true, encoding: .utf8) - XCTAssertNotNil( - try tryLoadCompilationDatabase( - directory: AbsolutePath(validating: "/a"), - fs - ) - ) + XCTAssertNotNil(tryLoadCompilationDatabase(directory: tempDir)) + } } - func testJSONCompilationDatabaseFromCustomDirectory() throws { - let fs = InMemoryFileSystem() - let root = try AbsolutePath(validating: "/a") - try fs.createDirectory(root) - XCTAssertNil(tryLoadCompilationDatabase(directory: root, fs)) + func testJSONCompilationDatabaseFromCustomDirectory() async throws { + try await withTestScratchDir { tempDir in + XCTAssertNil(tryLoadCompilationDatabase(directory: tempDir)) - let customDir = try RelativePath(validating: "custom/build/dir") - try fs.createDirectory(root.appending(customDir), recursive: true) + let customDir = try RelativePath(validating: "custom/build/dir") + try FileManager.default.createDirectory(at: tempDir.appending(customDir), withIntermediateDirectories: true) - try fs.writeFileContents( - root - .appending(customDir) - .appending(component: "compile_commands.json"), - bytes: """ - [ - { - "file": "/a/a.swift", - "directory": "/a", - "arguments": ["swiftc", "/a/a.swift"] - } - ] - """ - ) + try """ + [ + { + "file": "/a/a.swift", + "directory": "/a", + "arguments": ["swiftc", "/a/a.swift"] + } + ] + """.write( + to: tempDir.appending(customDir).appendingPathComponent("compile_commands.json"), + atomically: true, + encoding: .utf8 + ) - XCTAssertNotNil( - try tryLoadCompilationDatabase( - directory: AbsolutePath(validating: "/a"), - additionalSearchPaths: [ - RelativePath(validating: "."), - customDir, - ], - fs + XCTAssertNotNil( + try tryLoadCompilationDatabase( + directory: tempDir, + additionalSearchPaths: [ + RelativePath(validating: "."), + customDir, + ] + ) ) - ) + } } - func testFixedCompilationDatabase() throws { - let fs = InMemoryFileSystem() - try fs.createDirectory(try AbsolutePath(validating: "/a")) - XCTAssertNil( - try tryLoadCompilationDatabase( - directory: AbsolutePath(validating: "/a"), - fs - ) - ) + func testFixedCompilationDatabase() async throws { + try await withTestScratchDir { tempDir in + XCTAssertNil(tryLoadCompilationDatabase(directory: tempDir)) - try fs.writeFileContents( - try AbsolutePath(validating: "/a/compile_flags.txt"), - bytes: """ - -xc++ - -I - libwidget/include/ - """ - ) + try """ + -xc++ + -I + libwidget/include/ + """.write(to: tempDir.appendingPathComponent("compile_flags.txt"), atomically: true, encoding: .utf8) - let db = try XCTUnwrap(tryLoadCompilationDatabase(directory: AbsolutePath(validating: "/a"), fs)) + let db = try XCTUnwrap(tryLoadCompilationDatabase(directory: tempDir)) - // Note: Use `AbsolutePath(validating:).pathString` to normalize forward slashes to backslashes on Windows - XCTAssertEqual( - db[DocumentURI(filePath: "/a/b", isDirectory: false)], - [ - CompilationDatabase.Command( - directory: try AbsolutePath(validating: "/a").pathString, - filename: try AbsolutePath(validating: "/a/b").pathString, - commandLine: ["clang", "-xc++", "-I", "libwidget/include/", try AbsolutePath(validating: "/a/b").pathString], - output: nil - ) - ] - ) + let filePath = try tempDir.appendingPathComponent("a.c").filePath + XCTAssertEqual( + db[DocumentURI(filePath: filePath, isDirectory: false)], + [ + CompilationDatabase.Command( + directory: try tempDir.filePath, + filename: filePath, + commandLine: [ + "clang", "-xc++", "-I", "libwidget/include/", filePath, + ], + output: nil + ) + ] + ) + } } - func testInvalidCompilationDatabase() throws { - let fs = InMemoryFileSystem() - let dir = try AbsolutePath(validating: "/a") - try fs.createDirectory(dir) - try fs.writeFileContents(dir.appending(component: "compile_commands.json"), bytes: "") - - XCTAssertNil(tryLoadCompilationDatabase(directory: dir, fs)) + func testInvalidCompilationDatabase() async throws { + try await withTestScratchDir { tempDir in + try "".write(to: tempDir.appendingPathComponent("compile_commands.json"), atomically: true, encoding: .utf8) + XCTAssertNil(tryLoadCompilationDatabase(directory: tempDir)) + } } func testCompilationDatabaseBuildSystem() async throws { @@ -334,11 +312,11 @@ final class CompilationDatabaseTests: XCTestCase { """ ) { buildSystem in assertEqual( - try URL(fileURLWithPath: await buildSystem.indexStorePath?.pathString ?? "").filePath, + try await buildSystem.indexStorePath?.filePath, "\(pathSeparator)b" ) assertEqual( - try URL(fileURLWithPath: await buildSystem.indexDatabasePath?.pathString ?? "").filePath, + try await buildSystem.indexDatabasePath?.filePath, "\(pathSeparator)IndexDatabase" ) } @@ -366,7 +344,7 @@ final class CompilationDatabaseTests: XCTestCase { ] """ ) { buildSystem in - await assertEqual(buildSystem.indexStorePath, try AbsolutePath(validating: "/b")) + await assertEqual(buildSystem.indexStorePath, URL(fileURLWithPath: "/b")) } } @@ -382,7 +360,7 @@ final class CompilationDatabaseTests: XCTestCase { ] """ ) { buildSystem in - assertEqual(await buildSystem.indexStorePath, try AbsolutePath(validating: "/b")) + assertEqual(await buildSystem.indexStorePath, URL(fileURLWithPath: "/b")) } } @@ -425,11 +403,11 @@ final class CompilationDatabaseTests: XCTestCase { """ ) { buildSystem in assertEqual( - try URL(fileURLWithPath: await buildSystem.indexStorePath?.pathString ?? "").filePath, + try await buildSystem.indexStorePath?.filePath, "\(pathSeparator)b" ) assertEqual( - try URL(fileURLWithPath: await buildSystem.indexDatabasePath?.pathString ?? "").filePath, + try await buildSystem.indexDatabasePath?.filePath, "\(pathSeparator)IndexDatabase" ) } @@ -445,17 +423,16 @@ fileprivate var pathSeparator: String { } private func checkCompilationDatabaseBuildSystem( - _ compdb: ByteString, - block: (CompilationDatabaseBuildSystem) async throws -> () + _ compdb: String, + block: @Sendable (CompilationDatabaseBuildSystem) async throws -> () ) async throws { - let fs = InMemoryFileSystem() - try fs.createDirectory(AbsolutePath(validating: "/a")) - try fs.writeFileContents(AbsolutePath(validating: "/a/compile_commands.json"), bytes: compdb) - let buildSystem = CompilationDatabaseBuildSystem( - projectRoot: try AbsolutePath(validating: "/a"), - searchPaths: try [RelativePath(validating: ".")], - connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), - fileSystem: fs - ) - try await block(XCTUnwrap(buildSystem)) + try await withTestScratchDir { tempDir in + try compdb.write(to: tempDir.appendingPathComponent("compile_commands.json"), atomically: true, encoding: .utf8) + let buildSystem = CompilationDatabaseBuildSystem( + projectRoot: tempDir, + searchPaths: try [RelativePath(validating: ".")], + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP") + ) + try await block(XCTUnwrap(buildSystem)) + } } diff --git a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift index 4764d84fe..cb4920a00 100644 --- a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift @@ -25,6 +25,7 @@ import TSCExtensions import ToolchainRegistry import XCTest +import struct Basics.AbsolutePath import struct Basics.Triple import struct PackageModel.BuildFlags @@ -37,9 +38,9 @@ private var hostTriple: Triple { let toolchain = try await unwrap( ToolchainRegistry.forTesting.preferredToolchain(containing: [\.clang, \.clangd, \.sourcekitd, \.swift, \.swiftc]) ) - let destinationToolchainBinDir = try XCTUnwrap(toolchain.swiftc?.parentDirectory) + let destinationToolchainBinDir = try XCTUnwrap(toolchain.swiftc?.deletingLastPathComponent()) - let hostSDK = try SwiftSDK.hostSwiftSDK(.init(destinationToolchainBinDir)) + let hostSDK = try SwiftSDK.hostSwiftSDK(Basics.AbsolutePath(validating: destinationToolchainBinDir.filePath)) let hostSwiftPMToolchain = try UserToolchain(swiftSDK: hostSDK) return hostSwiftPMToolchain.targetTriple @@ -48,22 +49,21 @@ private var hostTriple: Triple { final class SwiftPMBuildSystemTests: XCTestCase { func testNoPackage() async throws { - let fs = InMemoryFileSystem() try await withTestScratchDir { tempDir in - try fs.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "" ] ) - let packageRoot = tempDir.appending(component: "pkg") - XCTAssertNil(SwiftPMBuildSystem.projectRoot(for: packageRoot.asURL, options: .testDefault())) + let packageRoot = tempDir.appendingPathComponent("pkg") + XCTAssertNil(SwiftPMBuildSystem.projectRoot(for: packageRoot, options: .testDefault())) } } func testNoToolchain() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -77,7 +77,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = tempDir.appending(component: "pkg") + let packageRoot = tempDir.appendingPathComponent("pkg") await assertThrowsError( try await SwiftPMBuildSystem( projectRoot: packageRoot, @@ -93,7 +93,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testBasicSwiftArgs() async throws { try await SkipUnless.swiftpmStoresModulesInSubdirectory() try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -107,7 +107,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) + let packageRoot = try tempDir.appendingPathComponent("pkg").realpath let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -117,14 +117,18 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") + let aswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("a.swift") let build = try await buildPath(root: packageRoot, platform: hostTriple.platformBuildPathComponent) assertNotNil(await buildSystemManager.initializationData?.indexDatabasePath) assertNotNil(await buildSystemManager.initializationData?.indexStorePath) let arguments = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) @@ -152,15 +156,15 @@ final class SwiftPMBuildSystemTests: XCTestCase { assertArgumentsContain("-target", try await hostTriple.tripleString, arguments: arguments) #endif - assertArgumentsContain("-I", build.appending(component: "Modules").pathString, arguments: arguments) + assertArgumentsContain("-I", try build.appendingPathComponent("Modules").filePath, arguments: arguments) - assertArgumentsContain(aswift.pathString, arguments: arguments) + assertArgumentsContain(try aswift.filePath, arguments: arguments) } } func testCompilerArgumentsForFileThatContainsPlusCharacterURLEncoded() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -175,7 +179,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = try AbsolutePath(validating: tempDir.appending(component: "pkg").asURL.realpath.filePath) + let packageRoot = try tempDir.appendingPathComponent("pkg").realpath let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -185,10 +189,14 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aPlusSomething = packageRoot.appending(components: "Sources", "lib", "a+something.swift") + let aPlusSomething = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("a+something.swift") assertNotNil(await buildSystemManager.initializationData?.indexStorePath) - let pathWithPlusEscaped = "\(try aPlusSomething.asURL.filePath.replacing("+", with: "%2B"))" + let pathWithPlusEscaped = "\(try aPlusSomething.filePath.replacing("+", with: "%2B"))" #if os(Windows) let urlWithPlusEscaped = try XCTUnwrap(URL(string: "file:///\(pathWithPlusEscaped)")) #else @@ -206,11 +214,14 @@ final class SwiftPMBuildSystemTests: XCTestCase { // Check that we have both source files in the compiler arguments, which means that we didn't compute the compiler // arguments for a+something.swift using substitute arguments from a.swift. XCTAssert( - arguments.contains(aPlusSomething.pathString), + try arguments.contains(aPlusSomething.filePath), "Compiler arguments do not contain a+something.swift: \(arguments)" ) XCTAssert( - arguments.contains(packageRoot.appending(components: "Sources", "lib", "a.swift").pathString), + try arguments.contains( + packageRoot.appendingPathComponent("Sources").appendingPathComponent("lib").appendingPathComponent("a.swift") + .filePath + ), "Compiler arguments do not contain a.swift: \(arguments)" ) } @@ -218,7 +229,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testBuildSetup() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -232,11 +243,11 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = tempDir.appending(component: "pkg") + let packageRoot = tempDir.appendingPathComponent("pkg") let options = SourceKitLSPOptions.SwiftPMOptions( configuration: .release, - scratchPath: packageRoot.appending(component: "non_default_build_path").pathString, + scratchPath: try packageRoot.appendingPathComponent("non_default_build_path").filePath, cCompilerFlags: ["-m32"], swiftCompilerFlags: ["-typecheck"] ) @@ -250,11 +261,15 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") + let aswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("a.swift") let arguments = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) @@ -268,7 +283,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testDefaultSDKs() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -290,7 +305,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) let swiftpmBuildSystem = try await SwiftPMBuildSystem( - projectRoot: tempDir.appending(component: "pkg"), + projectRoot: tempDir.appendingPathComponent("pkg"), toolchainRegistry: tr, options: SourceKitLSPOptions(swiftPM: options), connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy"), @@ -307,7 +322,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testManifestArgs() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -321,7 +336,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = tempDir.appending(component: "pkg") + let packageRoot = tempDir.appendingPathComponent("pkg") let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -331,23 +346,23 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let source = try resolveSymlinks(packageRoot.appending(component: "Package.swift")) + let source = try packageRoot.appendingPathComponent("Package.swift").realpath let arguments = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: source.asURI, + for: DocumentURI(source), language: .swift, fallbackAfterTimeout: false ) ).compilerArguments assertArgumentsContain("-swift-version", "4.2", arguments: arguments) - assertArgumentsContain(source.pathString, arguments: arguments) + assertArgumentsContain(try source.filePath, arguments: arguments) } } func testMultiFileSwift() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -361,7 +376,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) + let packageRoot = try tempDir.appendingPathComponent("pkg").realpath let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -371,33 +386,41 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") - let bswift = packageRoot.appending(components: "Sources", "lib", "b.swift") + let aswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("a.swift") + let bswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("b.swift") let argumentsA = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) ).compilerArguments - assertArgumentsContain(aswift.pathString, arguments: argumentsA) - assertArgumentsContain(bswift.pathString, arguments: argumentsA) + assertArgumentsContain(try aswift.filePath, arguments: argumentsA) + assertArgumentsContain(try bswift.filePath, arguments: argumentsA) let argumentsB = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) ).compilerArguments - assertArgumentsContain(aswift.pathString, arguments: argumentsB) - assertArgumentsContain(bswift.pathString, arguments: argumentsB) + assertArgumentsContain(try aswift.filePath, arguments: argumentsB) + assertArgumentsContain(try bswift.filePath, arguments: argumentsB) } } func testMultiTargetSwift() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/libA/a.swift": "", @@ -418,7 +441,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) + let packageRoot = try tempDir.appendingPathComponent("pkg").realpath let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -428,37 +451,53 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift") - let bswift = packageRoot.appending(components: "Sources", "libB", "b.swift") + let aswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("libA") + .appendingPathComponent("a.swift") + let bswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("libB") + .appendingPathComponent("b.swift") let arguments = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) ).compilerArguments - assertArgumentsContain(aswift.pathString, arguments: arguments) - assertArgumentsDoNotContain(bswift.pathString, arguments: arguments) + assertArgumentsContain(try aswift.filePath, arguments: arguments) + assertArgumentsDoNotContain(try bswift.filePath, arguments: arguments) assertArgumentsContain( "-Xcc", "-I", "-Xcc", - packageRoot.appending(components: "Sources", "libC", "include").pathString, + try packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("libC") + .appendingPathComponent("include") + .filePath, arguments: arguments ) let argumentsB = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: bswift.asURI, + for: DocumentURI(bswift), language: .swift, fallbackAfterTimeout: false ) ).compilerArguments - assertArgumentsContain(bswift.pathString, arguments: argumentsB) - assertArgumentsDoNotContain(aswift.pathString, arguments: argumentsB) + assertArgumentsContain(try bswift.filePath, arguments: argumentsB) + assertArgumentsDoNotContain(try aswift.filePath, arguments: argumentsB) assertArgumentsDoNotContain( "-I", - packageRoot.appending(components: "Sources", "libC", "include").pathString, + try packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("libC") + .appendingPathComponent("include") + .filePath, arguments: argumentsB ) } @@ -466,7 +505,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testUnknownFile() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/libA/a.swift": "", @@ -481,7 +520,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = tempDir.appending(component: "pkg") + let packageRoot = tempDir.appendingPathComponent("pkg") let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -491,18 +530,26 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift") - let bswift = packageRoot.appending(components: "Sources", "libB", "b.swift") + let aswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("libA") + .appendingPathComponent("a.swift") + let bswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("libB") + .appendingPathComponent("b.swift") assertNotNil( await buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) ) assertEqual( await buildSystemManager.buildSettingsInferredFromMainFile( - for: bswift.asURI, + for: DocumentURI(bswift), language: .swift, fallbackAfterTimeout: false )?.isFallback, @@ -521,7 +568,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testBasicCXXArgs() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.cpp": "", @@ -538,7 +585,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) + let packageRoot = try tempDir.appendingPathComponent("pkg").realpath let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -548,9 +595,22 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let acxx = packageRoot.appending(components: "Sources", "lib", "a.cpp") - let bcxx = packageRoot.appending(components: "Sources", "lib", "b.cpp") - let header = packageRoot.appending(components: "Sources", "lib", "include", "a.h") + let acxx = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("a.cpp") + let bcxx = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("b.cpp") + let header = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("include") + .appendingPathComponent("a.h") let build = buildPath(root: packageRoot, platform: try await hostTriple.platformBuildPathComponent) assertNotNil(await buildSystemManager.initializationData?.indexStorePath) @@ -558,7 +618,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { for file in [acxx, header] { let args = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: file.asURI, + for: DocumentURI(file), language: .cpp, fallbackAfterTimeout: false ) @@ -583,22 +643,26 @@ final class SwiftPMBuildSystemTests: XCTestCase { assertArgumentsContain( "-I", - packageRoot.appending(components: "Sources", "lib", "include").pathString, + try packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("include") + .filePath, arguments: args ) - assertArgumentsDoNotContain("-I", build.pathString, arguments: args) - assertArgumentsDoNotContain(bcxx.pathString, arguments: args) + assertArgumentsDoNotContain("-I", try build.filePath, arguments: args) + assertArgumentsDoNotContain(try bcxx.filePath, arguments: args) - URL(fileURLWithPath: build.appending(components: "lib.build", "a.cpp.d").pathString) + URL(fileURLWithPath: try build.appendingPathComponent("lib.build").appendingPathComponent("a.cpp.d").filePath) .withUnsafeFileSystemRepresentation { assertArgumentsContain("-MD", "-MT", "dependencies", "-MF", String(cString: $0!), arguments: args) } - URL(fileURLWithPath: file.pathString).withUnsafeFileSystemRepresentation { + URL(fileURLWithPath: try file.filePath).withUnsafeFileSystemRepresentation { assertArgumentsContain("-c", String(cString: $0!), arguments: args) } - URL(fileURLWithPath: build.appending(components: "lib.build", "a.cpp.o").pathString) + URL(fileURLWithPath: try build.appendingPathComponent("lib.build").appendingPathComponent("a.cpp.o").filePath) .withUnsafeFileSystemRepresentation { assertArgumentsContain("-o", String(cString: $0!), arguments: args) } @@ -608,8 +672,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testDeploymentTargetSwift() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( - root: tempDir, + try FileManager.default.createFiles( + root: try tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Package.swift": """ @@ -622,7 +686,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = tempDir.appending(component: "pkg") + let packageRoot = tempDir.appendingPathComponent("pkg") let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -632,10 +696,14 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") + let aswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("a.swift") let arguments = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) @@ -656,7 +724,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testSymlinkInWorkspaceSwift() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg_real/Sources/lib/a.swift": "", @@ -670,18 +738,18 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = tempDir.appending(component: "pkg") + let packageRoot = tempDir.appendingPathComponent("pkg") try FileManager.default.createSymbolicLink( - at: URL(fileURLWithPath: packageRoot.pathString), - withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").pathString) + at: URL(fileURLWithPath: packageRoot.filePath), + withDestinationURL: URL(fileURLWithPath: tempDir.appendingPathComponent("pkg_real").filePath) ) - let projectRoot = try XCTUnwrap(SwiftPMBuildSystem.projectRoot(for: packageRoot.asURL, options: .testDefault())) + let projectRoot = try XCTUnwrap(SwiftPMBuildSystem.projectRoot(for: packageRoot, options: .testDefault())) let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec( kind: .swiftPM, - projectRoot: try AbsolutePath(validating: projectRoot.filePath) + projectRoot: projectRoot ), toolchainRegistry: .forTesting, options: SourceKitLSPOptions(), @@ -690,20 +758,24 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswiftSymlink = packageRoot.appending(components: "Sources", "lib", "a.swift") - let aswiftReal = try resolveSymlinks(aswiftSymlink) - let manifest = packageRoot.appending(components: "Package.swift") + let aswiftSymlink = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("a.swift") + let aswiftReal = try aswiftSymlink.realpath + let manifest = packageRoot.appendingPathComponent("Package.swift") let argumentsFromSymlink = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswiftSymlink.asURI, + for: DocumentURI(aswiftSymlink), language: .swift, fallbackAfterTimeout: false ) ).compilerArguments let argumentsFromReal = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswiftReal.asURI, + for: DocumentURI(aswiftReal), language: .swift, fallbackAfterTimeout: false ) @@ -713,33 +785,33 @@ final class SwiftPMBuildSystemTests: XCTestCase { // contain they file the build settings were created. // FIXME: Or should the build settings always reference the main file? XCTAssertEqual( - argumentsFromSymlink.filter { $0 != aswiftSymlink.pathString && $0 != aswiftReal.pathString }, - argumentsFromReal.filter { $0 != aswiftSymlink.pathString && $0 != aswiftReal.pathString } + try argumentsFromSymlink.filter { try $0 != aswiftSymlink.filePath && $0 != aswiftReal.filePath }, + try argumentsFromReal.filter { try $0 != aswiftSymlink.filePath && $0 != aswiftReal.filePath } ) - assertArgumentsContain(aswiftSymlink.pathString, arguments: argumentsFromSymlink) - assertArgumentsDoNotContain(aswiftReal.pathString, arguments: argumentsFromSymlink) + assertArgumentsContain(try aswiftSymlink.filePath, arguments: argumentsFromSymlink) + assertArgumentsDoNotContain(try aswiftReal.filePath, arguments: argumentsFromSymlink) - assertArgumentsContain(aswiftReal.pathString, arguments: argumentsFromReal) - assertArgumentsDoNotContain(aswiftSymlink.pathString, arguments: argumentsFromReal) + assertArgumentsContain(try aswiftReal.filePath, arguments: argumentsFromReal) + assertArgumentsDoNotContain(try aswiftSymlink.filePath, arguments: argumentsFromReal) let argsManifest = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: manifest.asURI, + for: DocumentURI(manifest), language: .swift, fallbackAfterTimeout: false ) ).compilerArguments XCTAssertNotNil(argsManifest) - assertArgumentsContain(manifest.pathString, arguments: argsManifest) - assertArgumentsDoNotContain(try resolveSymlinks(manifest).pathString, arguments: argsManifest) + assertArgumentsContain(try manifest.filePath, arguments: argsManifest) + assertArgumentsDoNotContain(try manifest.realpath.filePath, arguments: argsManifest) } } func testSymlinkInWorkspaceCXX() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg_real/Sources/lib/a.cpp": "", @@ -760,19 +832,19 @@ final class SwiftPMBuildSystemTests: XCTestCase { let acpp = ["Sources", "lib", "a.cpp"] let ah = ["Sources", "lib", "include", "a.h"] - let realRoot = tempDir.appending(component: "pkg_real") - let symlinkRoot = tempDir.appending(component: "pkg") + let realRoot = tempDir.appendingPathComponent("pkg_real") + let symlinkRoot = tempDir.appendingPathComponent("pkg") try FileManager.default.createSymbolicLink( - at: URL(fileURLWithPath: symlinkRoot.pathString), - withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").pathString) + at: URL(fileURLWithPath: symlinkRoot.filePath), + withDestinationURL: URL(fileURLWithPath: tempDir.appendingPathComponent("pkg_real").filePath) ) - let projectRoot = try XCTUnwrap(SwiftPMBuildSystem.projectRoot(for: symlinkRoot.asURL, options: .testDefault())) + let projectRoot = try XCTUnwrap(SwiftPMBuildSystem.projectRoot(for: symlinkRoot, options: .testDefault())) let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec( kind: .swiftPM, - projectRoot: try AbsolutePath(validating: projectRoot.filePath) + projectRoot: projectRoot ), toolchainRegistry: .forTesting, options: SourceKitLSPOptions(), @@ -784,21 +856,21 @@ final class SwiftPMBuildSystemTests: XCTestCase { for file in [acpp, ah] { let args = try unwrap( await buildSystemManager.buildSettingsInferredFromMainFile( - for: symlinkRoot.appending(components: file).asURI, + for: DocumentURI(symlinkRoot.appending(components: file)), language: .cpp, fallbackAfterTimeout: false )? .compilerArguments ) - assertArgumentsDoNotContain(realRoot.appending(components: file).pathString, arguments: args) - assertArgumentsContain(symlinkRoot.appending(components: file).pathString, arguments: args) + assertArgumentsDoNotContain(try realRoot.appending(components: file).filePath, arguments: args) + assertArgumentsContain(try symlinkRoot.appending(components: file).filePath, arguments: args) } } } func testSwiftDerivedSources() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -813,7 +885,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) + let packageRoot = try tempDir.appendingPathComponent("pkg").realpath let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -823,16 +895,20 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") + let aswift = + packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("lib") + .appendingPathComponent("a.swift") let arguments = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) ) .compilerArguments - assertArgumentsContain(aswift.pathString, arguments: arguments) + assertArgumentsContain(try aswift.filePath, arguments: arguments) XCTAssertNotNil( arguments.firstIndex(where: { $0.hasSuffix(".swift") && $0.contains("DerivedSources") @@ -844,7 +920,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testNestedInvalidPackageSwift() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/Package.swift": "// not a valid package", @@ -858,16 +934,20 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let workspaceRoot = tempDir.appending(components: "pkg", "Sources", "lib").asURL + let workspaceRoot = + tempDir + .appendingPathComponent("pkg") + .appendingPathComponent("Sources") + .appendingPathComponent("lib") let projectRoot = SwiftPMBuildSystem.projectRoot(for: workspaceRoot, options: .testDefault()) - assertEqual(projectRoot, tempDir.appending(component: "pkg").asURL) + assertEqual(projectRoot, tempDir.appendingPathComponent("pkg", isDirectory: true)) } } func testPluginArgs() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Plugins/MyPlugin/a.swift": "", @@ -885,7 +965,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = tempDir.appending(component: "pkg") + let packageRoot = tempDir.appendingPathComponent("pkg") let buildSystemManager = await BuildSystemManager( buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -895,12 +975,16 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) await buildSystemManager.waitForUpToDateBuildGraph() - let aswift = packageRoot.appending(components: "Plugins", "MyPlugin", "a.swift") + let aswift = + packageRoot + .appendingPathComponent("Plugins") + .appendingPathComponent("MyPlugin") + .appendingPathComponent("a.swift") assertNotNil(await buildSystemManager.initializationData?.indexStorePath) let arguments = try await unwrap( buildSystemManager.buildSettingsInferredFromMainFile( - for: aswift.asURI, + for: DocumentURI(aswift), language: .swift, fallbackAfterTimeout: false ) @@ -908,7 +992,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { // Plugins get compiled with the same compiler arguments as the package manifest assertArgumentsContain("-package-description-version", "5.7.0", arguments: arguments) - assertArgumentsContain(aswift.pathString, arguments: arguments) + assertArgumentsContain(try aswift.filePath, arguments: arguments) } } @@ -1022,11 +1106,25 @@ private func assertArgumentsContain( } private func buildPath( - root: AbsolutePath, + root: URL, options: SourceKitLSPOptions.SwiftPMOptions = SourceKitLSPOptions.SwiftPMOptions(), platform: String -) -> AbsolutePath { +) -> URL { let buildPath = - AbsolutePath(validatingOrNil: options.scratchPath) ?? root.appending(components: ".build", "index-build") - return buildPath.appending(components: platform, "\(options.configuration ?? .debug)") + if let scratchPath = options.scratchPath { + URL(fileURLWithPath: scratchPath) + } else { + root.appendingPathComponent(".build").appendingPathComponent("index-build") + } + return buildPath.appendingPathComponent(platform).appendingPathComponent("\(options.configuration ?? .debug)") +} + +fileprivate extension URL { + func appending(components: [String]) -> URL { + var result = self + for component in components { + result.appendPathComponent(component) + } + return result + } } diff --git a/Tests/DiagnoseTests/DiagnoseTests.swift b/Tests/DiagnoseTests/DiagnoseTests.swift index 0374dff39..9e7ced6c5 100644 --- a/Tests/DiagnoseTests/DiagnoseTests.swift +++ b/Tests/DiagnoseTests/DiagnoseTests.swift @@ -161,8 +161,8 @@ final class DiagnoseTests: XCTestCase { func unrelatedB() {} """ - let fileAPath = scratchDir.appending(component: "a.swift").pathString - let fileBPath = scratchDir.appending(component: "b.swift").pathString + let fileAPath = try scratchDir.appendingPathComponent("a.swift").filePath + let fileBPath = try scratchDir.appendingPathComponent("b.swift").filePath try fileAContents.write(toFile: fileAPath, atomically: true, encoding: .utf8) try fileBContents.write(toFile: fileBPath, atomically: true, encoding: .utf8) @@ -239,7 +239,7 @@ private func assertReduceSourceKitD( let (markers, fileContents) = extractMarkers(markedFileContents) let toolchain = try await unwrap(ToolchainRegistry.forTesting.default) - logger.debug("Using \(toolchain.path?.pathString ?? "") to reduce source file") + logger.debug("Using \(toolchain.path?.description ?? "") to reduce source file") let markerOffset = try XCTUnwrap(markers["1️⃣"], "Failed to find position marker 1️⃣ in file contents") @@ -250,7 +250,7 @@ private func assertReduceSourceKitD( reproducerPredicate(requestResponse as! String) }) ) - let testFilePath = scratchDir.appending(component: "test.swift").pathString + let testFilePath = try scratchDir.appendingPathComponent("test.swift").filePath try fileContents.write(toFile: testFilePath, atomically: false, encoding: .utf8) let request = @@ -292,7 +292,7 @@ private class InProcessSourceKitRequestExecutor: SourceKitRequestExecutor { private let reproducerPredicate: NSPredicate init(toolchain: Toolchain, reproducerPredicate: NSPredicate) throws { - self.sourcekitd = try XCTUnwrap(toolchain.sourcekitd?.asURL) + self.sourcekitd = try XCTUnwrap(toolchain.sourcekitd) self.swiftFrontend = try XCTUnwrap(toolchain.swiftFrontend) self.reproducerPredicate = reproducerPredicate temporaryRequestFile = FileManager.default.temporaryDirectory.appendingPathComponent("request-\(UUID()).yml") @@ -317,9 +317,7 @@ private class InProcessSourceKitRequestExecutor: SourceKitRequestExecutor { let requestString = try request.request(for: temporarySourceFile) logger.info("Sending request: \(requestString)") - let sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate( - dylibPath: try! AbsolutePath(validating: sourcekitd.filePath) - ) + let sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(dylibPath: sourcekitd) let response = try await sourcekitd.run(requestYaml: requestString) logger.info("Received response: \(response.description)") diff --git a/Tests/SourceKitDTests/SourceKitDRegistryTests.swift b/Tests/SourceKitDTests/SourceKitDRegistryTests.swift index 4108a4482..b25660b0f 100644 --- a/Tests/SourceKitDTests/SourceKitDRegistryTests.swift +++ b/Tests/SourceKitDTests/SourceKitDRegistryTests.swift @@ -23,9 +23,9 @@ final class SourceKitDRegistryTests: XCTestCase { func testAdd() async throws { let registry = SourceKitDRegistry() - let a = try await FakeSourceKitD.getOrCreate(AbsolutePath(validating: "/a"), in: registry) - let b = try await FakeSourceKitD.getOrCreate(AbsolutePath(validating: "/b"), in: registry) - let a2 = try await FakeSourceKitD.getOrCreate(AbsolutePath(validating: "/a"), in: registry) + let a = await FakeSourceKitD.getOrCreate(URL(fileURLWithPath: "/a"), in: registry) + let b = await FakeSourceKitD.getOrCreate(URL(fileURLWithPath: "/b"), in: registry) + let a2 = await FakeSourceKitD.getOrCreate(URL(fileURLWithPath: "/a"), in: registry) XCTAssert(a === a2) XCTAssert(a !== b) @@ -34,9 +34,9 @@ final class SourceKitDRegistryTests: XCTestCase { func testRemove() async throws { let registry = SourceKitDRegistry() - let a = await FakeSourceKitD.getOrCreate(try AbsolutePath(validating: "/a"), in: registry) - await assertTrue(registry.remove(try AbsolutePath(validating: "/a")) === a) - await assertNil(registry.remove(try AbsolutePath(validating: "/a"))) + let a = await FakeSourceKitD.getOrCreate(URL(fileURLWithPath: "/a"), in: registry) + await assertTrue(registry.remove(URL(fileURLWithPath: "/a")) === a) + await assertNil(registry.remove(URL(fileURLWithPath: "/a"))) } func testRemoveResurrect() async throws { @@ -44,19 +44,19 @@ final class SourceKitDRegistryTests: XCTestCase { @inline(never) func scope(registry: SourceKitDRegistry) async throws -> UInt32 { - let a = await FakeSourceKitD.getOrCreate(try AbsolutePath(validating: "/a"), in: registry) + let a = await FakeSourceKitD.getOrCreate(URL(fileURLWithPath: "/a"), in: registry) - await assertTrue(a === FakeSourceKitD.getOrCreate(try AbsolutePath(validating: "/a"), in: registry)) - await assertTrue(registry.remove(try AbsolutePath(validating: "/a")) === a) + await assertTrue(a === FakeSourceKitD.getOrCreate(URL(fileURLWithPath: "/a"), in: registry)) + await assertTrue(registry.remove(URL(fileURLWithPath: "/a")) === a) // Resurrected. - await assertTrue(a === FakeSourceKitD.getOrCreate(try AbsolutePath(validating: "/a"), in: registry)) + await assertTrue(a === FakeSourceKitD.getOrCreate(URL(fileURLWithPath: "/a"), in: registry)) // Remove again. - await assertTrue(registry.remove(try AbsolutePath(validating: "/a")) === a) + await assertTrue(registry.remove(URL(fileURLWithPath: "/a")) === a) return (a as! FakeSourceKitD).token } let id = try await scope(registry: registry) - let a2 = await FakeSourceKitD.getOrCreate(try AbsolutePath(validating: "/a"), in: registry) + let a2 = await FakeSourceKitD.getOrCreate(URL(fileURLWithPath: "/a"), in: registry) XCTAssertNotEqual(id, (a2 as! FakeSourceKitD).token) } } @@ -75,8 +75,8 @@ final class FakeSourceKitD: SourceKitD { token = nextToken.fetchAndIncrement() } - static func getOrCreate(_ path: AbsolutePath, in registry: SourceKitDRegistry) async -> SourceKitD { - return await registry.getOrAdd(path, create: { Self.init() }) + static func getOrCreate(_ url: URL, in registry: SourceKitDRegistry) async -> SourceKitD { + return await registry.getOrAdd(url, create: { Self.init() }) } package func log(request: SKDRequestDictionary) {} diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 442ac5ec6..7a8ebd626 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -1245,7 +1245,7 @@ final class BackgroundIndexingTests: XCTestCase { // - We reload the package, which updates `Dependency.swift` in `.build/index-build/checkouts`, which we also watch. try await Process.run( arguments: [ - unwrap(ToolchainRegistry.forTesting.default?.swift?.pathString), + unwrap(ToolchainRegistry.forTesting.default?.swift?.filePath), "package", "update", "--package-path", project.scratchDirectory.filePath, ], diff --git a/Tests/SourceKitLSPTests/BuildSystemTests.swift b/Tests/SourceKitLSPTests/BuildSystemTests.swift index 385ec8b83..ed86baa3b 100644 --- a/Tests/SourceKitLSPTests/BuildSystemTests.swift +++ b/Tests/SourceKitLSPTests/BuildSystemTests.swift @@ -49,7 +49,7 @@ final class BuildSystemTests: XCTestCase { let server = testClient.server let buildSystemManager = await BuildSystemManager( - buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: try AbsolutePath(validating: "/")), + buildSystemSpec: BuildSystemSpec(kind: .testBuildSystem, projectRoot: URL(fileURLWithPath: "/")), toolchainRegistry: .forTesting, options: .testDefault(), connectionToClient: DummyBuildSystemManagerConnectionToClient(), diff --git a/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift b/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift index 5a7dfba30..90d29cbe3 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift @@ -782,7 +782,7 @@ final class WorkspaceTestDiscoveryTests: XCTestCase { let testsWithEmptyCompilationDatabase = try await project.testClient.send(WorkspaceTestsRequest()) XCTAssertEqual(testsWithEmptyCompilationDatabase, []) - let swiftc = try await unwrap(ToolchainRegistry.forTesting.default?.swiftc?.asURL) + let swiftc = try await unwrap(ToolchainRegistry.forTesting.default?.swiftc) let uri = try project.uri(for: "MyTests.swift") let compilationDatabase = JSONCompilationDatabase([ diff --git a/Tests/SourceKitLSPTests/WorkspaceTests.swift b/Tests/SourceKitLSPTests/WorkspaceTests.swift index 9c5e30ea4..84a1187bc 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTests.swift @@ -688,7 +688,7 @@ final class WorkspaceTests: XCTestCase { let packageDir = try project.uri(for: "Package.swift").fileURL!.deletingLastPathComponent() try await TSCBasic.Process.checkNonZeroExit(arguments: [ - ToolchainRegistry.forTesting.default!.swift!.pathString, + ToolchainRegistry.forTesting.default!.swift!.filePath, "build", "--package-path", packageDir.filePath, "-Xswiftc", "-index-ignore-system-modules", diff --git a/Tests/TSCExtensionsTests/ProcessRunTests.swift b/Tests/TSCExtensionsTests/ProcessRunTests.swift index 0bda5beaf..ddca3e02b 100644 --- a/Tests/TSCExtensionsTests/ProcessRunTests.swift +++ b/Tests/TSCExtensionsTests/ProcessRunTests.swift @@ -21,8 +21,8 @@ import class TSCBasic.Process final class ProcessRunTests: XCTestCase { func testWorkingDirectory() async throws { try await withTestScratchDir { tempDir in - let workingDir = tempDir.appending(component: "working-dir") - try FileManager.default.createDirectory(at: workingDir.asURL, withIntermediateDirectories: true) + let workingDir = tempDir.appendingPathComponent("working-dir") + try FileManager.default.createDirectory(at: workingDir, withIntermediateDirectories: true) #if os(Windows) // On Windows, Python 3 gets installed as python.exe @@ -32,18 +32,18 @@ final class ProcessRunTests: XCTestCase { #endif let python = try await unwrap(findTool(name: pythonName)) - let pythonFile = tempDir.appending(component: "show-cwd.py") + let pythonFile = tempDir.appendingPathComponent("show-cwd.py") try """ import os print(os.getcwd(), end='') - """.write(to: pythonFile.asURL, atomically: true, encoding: .utf8) + """.write(to: pythonFile, atomically: true, encoding: .utf8) let result = try await Process.run( - arguments: [python.filePath, pythonFile.pathString], - workingDirectory: workingDir + arguments: [python.filePath, pythonFile.filePath], + workingDirectory: AbsolutePath(validating: workingDir.filePath) ) let stdout = try unwrap(String(bytes: result.output.get(), encoding: .utf8)) - XCTAssertEqual(stdout, workingDir.pathString) + XCTAssertEqual(stdout, try workingDir.filePath) } } } diff --git a/Tests/ToolchainRegistryTests/ToolchainRegistryTests.swift b/Tests/ToolchainRegistryTests/ToolchainRegistryTests.swift index 220c5391e..10b01f55c 100644 --- a/Tests/ToolchainRegistryTests/ToolchainRegistryTests.swift +++ b/Tests/ToolchainRegistryTests/ToolchainRegistryTests.swift @@ -42,329 +42,391 @@ final class ToolchainRegistryTests: XCTestCase { func testFindXcodeDefaultToolchain() async throws { try SkipUnless.platformIsDarwin("Finding toolchains in Xcode is only supported on macOS") - let fs = InMemoryFileSystem() - let xcodeDeveloper = try AbsolutePath(validating: "/Applications/Xcode.app/Developer") - let toolchains = xcodeDeveloper.appending(components: "Toolchains") - try makeXCToolchain( - identifier: ToolchainRegistry.darwinDefaultToolchainIdentifier, - opensource: false, - path: toolchains.appending(component: "XcodeDefault.xctoolchain"), - fs, - sourcekitd: true - ) - let tr = ToolchainRegistry( - xcodes: [xcodeDeveloper], - darwinToolchainOverride: nil, - fs - ) + try await withTestScratchDir { tempDir in + let xcodeDeveloper = + tempDir + .appendingPathComponent("Xcode.app") + .appendingPathComponent("Developer") + let toolchains = xcodeDeveloper.appendingPathComponent("Toolchains") + try makeXCToolchain( + identifier: ToolchainRegistry.darwinDefaultToolchainIdentifier, + opensource: false, + path: toolchains.appendingPathComponent("XcodeDefault.xctoolchain"), + sourcekitd: true + ) - assertEqual(await tr.toolchains.count, 1) - assertEqual(await tr.default?.identifier, ToolchainRegistry.darwinDefaultToolchainIdentifier) - assertEqual(await tr.default?.path, toolchains.appending(component: "XcodeDefault.xctoolchain")) - assertNotNil(await tr.default?.sourcekitd) + let tr = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [xcodeDeveloper], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) - assertTrue(await tr.toolchains.first === tr.default) + assertEqual(await tr.toolchains.count, 1) + assertEqual(await tr.default?.identifier, ToolchainRegistry.darwinDefaultToolchainIdentifier) + assertEqual( + await tr.default?.path, + toolchains.appendingPathComponent("XcodeDefault.xctoolchain", isDirectory: true) + ) + assertNotNil(await tr.default?.sourcekitd) + + assertTrue(await tr.toolchains.first === tr.default) + } } func testFindNonXcodeDefaultToolchains() async throws { try SkipUnless.platformIsDarwin("Finding toolchains in Xcode is only supported on macOS") - let fs = InMemoryFileSystem() - let xcodeDeveloper = try AbsolutePath(validating: "/Applications/Xcode.app/Developer") - let toolchains = xcodeDeveloper.appending(components: "Toolchains") - - try makeXCToolchain( - identifier: "com.apple.fake.A", - opensource: false, - path: toolchains.appending(component: "A.xctoolchain"), - fs, - sourcekitd: true - ) - try makeXCToolchain( - identifier: "com.apple.fake.B", - opensource: false, - path: toolchains.appending(component: "B.xctoolchain"), - fs, - sourcekitd: true - ) - let tr = ToolchainRegistry( - xcodes: [xcodeDeveloper], - darwinToolchainOverride: nil, - fs - ) + try await withTestScratchDir { tempDir in + let xcodeDeveloper = + tempDir + .appendingPathComponent("Xcode.app") + .appendingPathComponent("Developer") + let toolchains = xcodeDeveloper.appendingPathComponent("Toolchains") + + try makeXCToolchain( + identifier: "com.apple.fake.A", + opensource: false, + path: toolchains.appendingPathComponent("A.xctoolchain"), + sourcekitd: true + ) + try makeXCToolchain( + identifier: "com.apple.fake.B", + opensource: false, + path: toolchains.appendingPathComponent("B.xctoolchain"), + sourcekitd: true + ) - assertEqual(await tr.toolchains.map(\.identifier).sorted(), ["com.apple.fake.A", "com.apple.fake.B"]) + let tr = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [xcodeDeveloper], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) + + assertEqual(await tr.toolchains.map(\.identifier).sorted(), ["com.apple.fake.A", "com.apple.fake.B"]) + } } func testIgnoreToolchainsWithWrongExtensions() async throws { try SkipUnless.platformIsDarwin("Finding toolchains in Xcode is only supported on macOS") - let fs = InMemoryFileSystem() - let xcodeDeveloper = try AbsolutePath(validating: "/Applications/Xcode.app/Developer") - let toolchains = xcodeDeveloper.appending(components: "Toolchains") - - try makeXCToolchain( - identifier: "com.apple.fake.C", - opensource: false, - path: toolchains.appending(component: "C.wrong_extension"), - fs, - sourcekitd: true - ) - try makeXCToolchain( - identifier: "com.apple.fake.D", - opensource: false, - path: toolchains.appending(component: "D_no_extension"), - fs, - sourcekitd: true - ) - let tr = ToolchainRegistry( - darwinToolchainOverride: nil, - fs - ) + try await withTestScratchDir { tempDir in + let xcodeDeveloper = + tempDir + .appendingPathComponent("Xcode.app") + .appendingPathComponent("Developer") + let toolchains = xcodeDeveloper.appendingPathComponent("Toolchains") + + try makeXCToolchain( + identifier: "com.apple.fake.C", + opensource: false, + path: toolchains.appendingPathComponent("C.wrong_extension"), + sourcekitd: true + ) + try makeXCToolchain( + identifier: "com.apple.fake.D", + opensource: false, + path: toolchains.appendingPathComponent("D_no_extension"), + sourcekitd: true + ) - assertTrue(await tr.toolchains.isEmpty) + let tr = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [xcodeDeveloper], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) + assertEqual(await tr.toolchains.map(\.path), []) + } } + func testTwoToolchainsWithSameIdentifier() async throws { try SkipUnless.platformIsDarwin("Finding toolchains in Xcode is only supported on macOS") - let fs = InMemoryFileSystem() - let xcodeDeveloper = try AbsolutePath(validating: "/Applications/Xcode.app/Developer") - let toolchains = xcodeDeveloper.appending(components: "Toolchains") - try makeXCToolchain( - identifier: "com.apple.fake.A", - opensource: false, - path: toolchains.appending(component: "A.xctoolchain"), - fs, - sourcekitd: true - ) + try await withTestScratchDir { tempDir in + let xcodeDeveloper = + tempDir + .appendingPathComponent("Xcode.app") + .appendingPathComponent("Developer") + + let toolchains = xcodeDeveloper.appendingPathComponent("Toolchains") + try makeXCToolchain( + identifier: "com.apple.fake.A", + opensource: false, + path: toolchains.appendingPathComponent("A.xctoolchain"), + sourcekitd: true + ) - try makeXCToolchain( - identifier: "com.apple.fake.A", - opensource: false, - path: toolchains.appending(component: "E.xctoolchain"), - fs, - sourcekitd: true - ) + try makeXCToolchain( + identifier: "com.apple.fake.A", + opensource: false, + path: toolchains.appendingPathComponent("E.xctoolchain"), + sourcekitd: true + ) - let tr = ToolchainRegistry( - xcodes: [xcodeDeveloper], - darwinToolchainOverride: nil, - fs - ) + let tr = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [xcodeDeveloper], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) - assertEqual(await tr.toolchains.count, 1) + assertEqual(await tr.toolchains.count, 1) + } } func testGloballyInstalledToolchains() async throws { try SkipUnless.platformIsDarwin("Finding toolchains in Xcode is only supported on macOS") - let fs = InMemoryFileSystem() - - try makeXCToolchain( - identifier: "org.fake.global.A", - opensource: true, - path: try AbsolutePath(validating: "/Library/Developer/Toolchains/A.xctoolchain"), - fs, - sourcekitd: true - ) - try makeXCToolchain( - identifier: "org.fake.global.B", - opensource: true, - path: try AbsolutePath( - validating: ("~/Library/Developer/Toolchains/B.xctoolchain" as NSString).expandingTildeInPath - ), - fs, - sourcekitd: true - ) + try await withTestScratchDir { tempDir in + let libraryDir = tempDir.appendingPathComponent("Library") + try makeXCToolchain( + identifier: "org.fake.global.A", + opensource: true, + path: + libraryDir + .appendingPathComponent("Developer") + .appendingPathComponent("Toolchains") + .appendingPathComponent("A.xctoolchain"), + sourcekitd: true + ) - let tr = ToolchainRegistry( - darwinToolchainOverride: nil, - fs - ) - assertEqual(await tr.toolchains.map(\.identifier), ["org.fake.global.B", "org.fake.global.A"]) + let tr = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [], + libraryDirectories: [libraryDir], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) + assertEqual(await tr.toolchains.map(\.identifier), ["org.fake.global.A"]) + } } func testFindToolchainBasedOnInstallPath() async throws { try SkipUnless.platformIsDarwin("Finding toolchains in Xcode is only supported on macOS") - let fs = InMemoryFileSystem() - let xcodeDeveloper = try AbsolutePath(validating: "/Applications/Xcode.app/Developer") - let toolchains = xcodeDeveloper.appending(components: "Toolchains") - - let path = toolchains.appending(component: "Explicit.xctoolchain") - try makeXCToolchain( - identifier: "org.fake.explicit", - opensource: false, - path: toolchains.appending(component: "Explicit.xctoolchain"), - fs, - sourcekitd: true - ) - let trInstall = ToolchainRegistry( - installPath: path.appending(components: "usr", "bin"), - xcodes: [], - darwinToolchainOverride: nil, - fs - ) - await assertEqual(trInstall.default?.identifier, "org.fake.explicit") - await assertEqual(trInstall.default?.path, path) + try await withTestScratchDir { tempDir in + let xcodeDeveloper = + tempDir + .appendingPathComponent("Xcode.app") + .appendingPathComponent("Developer") + + let toolchains = xcodeDeveloper.appendingPathComponent("Toolchains") + + let path = toolchains.appendingPathComponent("Explicit.xctoolchain", isDirectory: true) + try makeXCToolchain( + identifier: "org.fake.explicit", + opensource: false, + path: path, + sourcekitd: true + ) + + let trInstall = ToolchainRegistry( + installPath: path.appendingPathComponent("usr").appendingPathComponent("bin"), + environmentVariables: [], + xcodes: [], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) + await assertEqual(trInstall.default?.identifier, "org.fake.explicit") + await assertEqual(trInstall.default?.path, path) + } } func testDarwinToolchainOverride() async throws { try SkipUnless.platformIsDarwin("Finding toolchains in Xcode is only supported on macOS") - let fs = InMemoryFileSystem() - let xcodeDeveloper = try AbsolutePath(validating: "/Applications/Xcode.app/Developer") - let toolchains = xcodeDeveloper.appending(components: "Toolchains") - try makeXCToolchain( - identifier: ToolchainRegistry.darwinDefaultToolchainIdentifier, - opensource: false, - path: toolchains.appending(component: "XcodeDefault.xctoolchain"), - fs, - sourcekitd: true - ) + try await withTestScratchDir { tempDir in + let xcodeDeveloper = + tempDir + .appendingPathComponent("Xcode.app") + .appendingPathComponent("Developer") + + let toolchains = xcodeDeveloper.appendingPathComponent("Toolchains") + try makeXCToolchain( + identifier: ToolchainRegistry.darwinDefaultToolchainIdentifier, + opensource: false, + path: toolchains.appendingPathComponent("XcodeDefault.xctoolchain"), + sourcekitd: true + ) - try makeXCToolchain( - identifier: "org.fake.global.A", - opensource: false, - path: toolchains.appending(component: "A.xctoolchain"), - fs, - sourcekitd: true - ) + try makeXCToolchain( + identifier: "org.fake.global.A", + opensource: false, + path: toolchains.appendingPathComponent("A.xctoolchain"), + sourcekitd: true + ) - let toolchainRegistry = ToolchainRegistry( - xcodes: [xcodeDeveloper], - darwinToolchainOverride: nil, - fs - ) - await assertEqual(toolchainRegistry.default?.identifier, ToolchainRegistry.darwinDefaultToolchainIdentifier) + let toolchainRegistry = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [xcodeDeveloper], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) - let darwinToolchainOverrideRegistry = ToolchainRegistry( - xcodes: [xcodeDeveloper], - darwinToolchainOverride: "org.fake.global.A", - fs - ) - await assertEqual(darwinToolchainOverrideRegistry.darwinToolchainIdentifier, "org.fake.global.A") - await assertEqual(darwinToolchainOverrideRegistry.default?.identifier, "org.fake.global.A") + await assertEqual(toolchainRegistry.default?.identifier, ToolchainRegistry.darwinDefaultToolchainIdentifier) + + let darwinToolchainOverrideRegistry = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [xcodeDeveloper], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: "org.fake.global.A" + ) + await assertEqual(darwinToolchainOverrideRegistry.darwinToolchainIdentifier, "org.fake.global.A") + await assertEqual(darwinToolchainOverrideRegistry.default?.identifier, "org.fake.global.A") + } } func testCreateToolchainFromBinPath() async throws { try SkipUnless.platformIsDarwin("Finding toolchains in Xcode is only supported on macOS") - let fs = InMemoryFileSystem() - let xcodeDeveloper = try AbsolutePath(validating: "/Applications/Xcode.app/Developer") - let toolchains = xcodeDeveloper.appending(components: "Toolchains") - - let path = toolchains.appending(component: "Explicit.xctoolchain") - try makeXCToolchain( - identifier: "org.fake.explicit", - opensource: false, - path: toolchains.appending(component: "Explicit.xctoolchain"), - fs, - sourcekitd: true - ) + try await withTestScratchDir { tempDir in + let xcodeDeveloper = + tempDir + .appendingPathComponent("Xcode.app") + .appendingPathComponent("Developer") + let toolchains = xcodeDeveloper.appendingPathComponent("Toolchains") + + let path = toolchains.appendingPathComponent("Explicit.xctoolchain", isDirectory: true) + try makeXCToolchain( + identifier: "org.fake.explicit", + opensource: false, + path: path, + sourcekitd: true + ) - let tc = Toolchain(path, fs) - XCTAssertNotNil(tc) - XCTAssertEqual(tc?.identifier, "org.fake.explicit") + let tc = Toolchain(path) + XCTAssertNotNil(tc) + XCTAssertEqual(tc?.identifier, "org.fake.explicit") - let tcBin = Toolchain(path.appending(components: "usr", "bin"), fs) - XCTAssertNotNil(tcBin) - XCTAssertEqual(tc?.identifier, tcBin?.identifier) - XCTAssertEqual(tc?.path, tcBin?.path) - XCTAssertEqual(tc?.displayName, tcBin?.displayName) + let tcBin = Toolchain(path.appendingPathComponent("usr").appendingPathComponent("bin")) + XCTAssertNotNil(tcBin) + XCTAssertEqual(tc?.identifier, tcBin?.identifier) + XCTAssertEqual(tc?.path, tcBin?.path) + XCTAssertEqual(tc?.displayName, tcBin?.displayName) + } } func testSearchPATH() async throws { - let fs = InMemoryFileSystem() - let binPath = try AbsolutePath(validating: "/foo/bar/my_toolchain/bin") - try makeToolchain(binPath: binPath, fs, sourcekitd: true) - - #if os(Windows) - let separator: String = ";" - #else - let separator: String = ":" - #endif + try await withTestScratchDir { tempDir in + let binPath = tempDir.appendingPathComponent("bin", isDirectory: true) + try makeToolchain(binPath: binPath, sourcekitd: true) - try ProcessEnv.setVar( - "SOURCEKIT_PATH", - value: ["/bogus", binPath.pathString, "/bogus2"].joined(separator: separator) - ) - defer { try! ProcessEnv.setVar("SOURCEKIT_PATH", value: "") } + #if os(Windows) + let separator: String = ";" + #else + let separator: String = ":" + #endif - let tr = ToolchainRegistry(fs) + try ProcessEnv.setVar( + "SOURCEKIT_PATH_FOR_TEST", + value: ["/bogus", binPath.filePath, "/bogus2"].joined(separator: separator) + ) + defer { try! ProcessEnv.setVar("SOURCEKIT_PATH_FOR_TEST", value: "") } + + let tr = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [], + libraryDirectories: [], + pathEnvironmentVariables: ["SOURCEKIT_PATH_FOR_TEST"], + darwinToolchainOverride: nil + ) - let tc = try unwrap(await tr.toolchains.first(where: { tc in tc.path == binPath })) + let tc = try unwrap(await tr.toolchains.first(where: { $0.path == binPath })) - await assertEqual(tr.default?.identifier, tc.identifier) - XCTAssertEqual(tc.identifier, binPath.pathString) - XCTAssertNil(tc.clang) - XCTAssertNil(tc.clangd) - XCTAssertNil(tc.swiftc) - XCTAssertNotNil(tc.sourcekitd) - XCTAssertNil(tc.libIndexStore) + await assertEqual(tr.default?.identifier, tc.identifier) + XCTAssertEqual(tc.identifier, try binPath.filePath) + XCTAssertNil(tc.clang) + XCTAssertNil(tc.clangd) + XCTAssertNil(tc.swiftc) + XCTAssertNotNil(tc.sourcekitd) + XCTAssertNil(tc.libIndexStore) + } } func testSearchExplicitEnvBuiltin() async throws { - let fs = InMemoryFileSystem() - - let binPath = try AbsolutePath(validating: "/foo/bar/my_toolchain/bin") - try makeToolchain(binPath: binPath, fs, sourcekitd: true) - - try ProcessEnv.setVar("TEST_SOURCEKIT_TOOLCHAIN_PATH_1", value: binPath.parentDirectory.pathString) + try await withTestScratchDir { tempDir in + let binPath = tempDir.appendingPathComponent("bin", isDirectory: true) + try makeToolchain(binPath: binPath, sourcekitd: true) + + try ProcessEnv.setVar("TEST_SOURCEKIT_TOOLCHAIN_PATH_1", value: binPath.deletingLastPathComponent().filePath) + + let tr = ToolchainRegistry( + installPath: nil, + environmentVariables: ["TEST_SOURCEKIT_TOOLCHAIN_PATH_1"], + xcodes: [], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) - let tr = ToolchainRegistry( - environmentVariables: ["TEST_SOURCEKIT_TOOLCHAIN_PATH_1"], - fs - ) + guard let tc = await tr.toolchains.first(where: { tc in tc.path == binPath.deletingLastPathComponent() }) else { + XCTFail("couldn't find expected toolchain") + return + } - guard let tc = await tr.toolchains.first(where: { tc in tc.path == binPath.parentDirectory }) else { - XCTFail("couldn't find expected toolchain") - return + await assertEqual(tr.default?.identifier, tc.identifier) + XCTAssertEqual(tc.identifier, try binPath.deletingLastPathComponent().filePath) + XCTAssertNil(tc.clang) + XCTAssertNil(tc.clangd) + XCTAssertNil(tc.swiftc) + XCTAssertNotNil(tc.sourcekitd) + XCTAssertNil(tc.libIndexStore) } - - await assertEqual(tr.default?.identifier, tc.identifier) - XCTAssertEqual(tc.identifier, binPath.parentDirectory.pathString) - XCTAssertNil(tc.clang) - XCTAssertNil(tc.clangd) - XCTAssertNil(tc.swiftc) - XCTAssertNotNil(tc.sourcekitd) - XCTAssertNil(tc.libIndexStore) } func testSearchExplicitEnv() async throws { - let fs = InMemoryFileSystem() - let binPath = try AbsolutePath(validating: "/foo/bar/my_toolchain/bin") - try makeToolchain(binPath: binPath, fs, sourcekitd: true) - - try ProcessEnv.setVar("TEST_ENV_SOURCEKIT_TOOLCHAIN_PATH_2", value: binPath.parentDirectory.pathString) + try await withTestScratchDir { tempDir in + let binPath = tempDir.appendingPathComponent("bin", isDirectory: true) + try makeToolchain(binPath: binPath, sourcekitd: true) + + try ProcessEnv.setVar("TEST_ENV_SOURCEKIT_TOOLCHAIN_PATH_2", value: binPath.deletingLastPathComponent().filePath) + + let tr = ToolchainRegistry( + installPath: nil, + environmentVariables: ["TEST_ENV_SOURCEKIT_TOOLCHAIN_PATH_2"], + xcodes: [], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) - let tr = ToolchainRegistry( - environmentVariables: ["TEST_ENV_SOURCEKIT_TOOLCHAIN_PATH_2"], - fs - ) + guard let tc = await tr.toolchains.first(where: { tc in tc.path == binPath.deletingLastPathComponent() }) else { + XCTFail("couldn't find expected toolchain") + return + } - guard let tc = await tr.toolchains.first(where: { tc in tc.path == binPath.parentDirectory }) else { - XCTFail("couldn't find expected toolchain") - return + XCTAssertEqual(tc.identifier, try binPath.deletingLastPathComponent().filePath) + XCTAssertNil(tc.clang) + XCTAssertNil(tc.clangd) + XCTAssertNil(tc.swiftc) + XCTAssertNotNil(tc.sourcekitd) + XCTAssertNil(tc.libIndexStore) } - - XCTAssertEqual(tc.identifier, binPath.parentDirectory.pathString) - XCTAssertNil(tc.clang) - XCTAssertNil(tc.clangd) - XCTAssertNil(tc.swiftc) - XCTAssertNotNil(tc.sourcekitd) - XCTAssertNil(tc.libIndexStore) } func testFromDirectory() async throws { - // This test uses the real file system because the in-memory system doesn't support marking files executable. - let fs = localFileSystem try await withTestScratchDir { tempDir in - let path = tempDir.appending(components: "A.xctoolchain", "usr") + let path = tempDir.appendingPathComponent("A.xctoolchain").appendingPathComponent("usr") try makeToolchain( - binPath: path.appending(component: "bin"), - fs, + binPath: path.appendingPathComponent("bin"), clang: true, clangd: true, swiftc: true, @@ -372,9 +434,9 @@ final class ToolchainRegistryTests: XCTestCase { sourcekitd: true ) - try fs.writeFileContents(path.appending(components: "bin", "other"), bytes: "") + try Data().write(to: path.appendingPathComponent("bin").appendingPathComponent("other")) - let t1 = Toolchain(path.parentDirectory, fs)! + let t1 = try XCTUnwrap(Toolchain(path.deletingLastPathComponent())) XCTAssertNotNil(t1.sourcekitd) #if os(Windows) // Windows does not have file permissions but rather checks the contents @@ -389,23 +451,23 @@ final class ToolchainRegistryTests: XCTestCase { #endif #if !os(Windows) - func chmodRX(_ path: AbsolutePath) { - XCTAssertEqual(chmod(path.pathString, S_IRUSR | S_IXUSR), 0) + func chmodRX(_ path: URL) throws { + XCTAssertEqual(chmod(try path.filePath, S_IRUSR | S_IXUSR), 0) } - chmodRX(path.appending(components: "bin", "clang")) - chmodRX(path.appending(components: "bin", "clangd")) - chmodRX(path.appending(components: "bin", "swiftc")) - chmodRX(path.appending(components: "bin", "other")) + try chmodRX(path.appendingPathComponent("bin").appendingPathComponent("clang")) + try chmodRX(path.appendingPathComponent("bin").appendingPathComponent("clangd")) + try chmodRX(path.appendingPathComponent("bin").appendingPathComponent("swiftc")) + try chmodRX(path.appendingPathComponent("bin").appendingPathComponent("other")) #endif - let t2 = Toolchain(path.parentDirectory, fs)! + let t2 = try XCTUnwrap(Toolchain(path.deletingLastPathComponent())) XCTAssertNotNil(t2.sourcekitd) XCTAssertNotNil(t2.clang) XCTAssertNotNil(t2.clangd) XCTAssertNotNil(t2.swiftc) - let tr = ToolchainRegistry(toolchains: [Toolchain(path.parentDirectory, fs)!]) + let tr = ToolchainRegistry(toolchains: [try XCTUnwrap(Toolchain(path.deletingLastPathComponent()))]) let t3 = try await unwrap(tr.toolchains(withIdentifier: t2.identifier).only) XCTAssertEqual(t3.sourcekitd, t2.sourcekitd) XCTAssertEqual(t3.clang, t2.clang) @@ -414,33 +476,46 @@ final class ToolchainRegistryTests: XCTestCase { } } - func testDylibNames() throws { - let fs = InMemoryFileSystem() - let binPath = try AbsolutePath(validating: "/foo/bar/my_toolchain/bin") - try makeToolchain(binPath: binPath, fs, sourcekitdInProc: true, libIndexStore: true) - guard let t = Toolchain(binPath, fs) else { - XCTFail("could not find any tools") - return + func testDylibNames() async throws { + try await withTestScratchDir { tempDir in + let binPath = tempDir.appendingPathComponent("bin", isDirectory: true) + try makeToolchain(binPath: binPath, sourcekitdInProc: true, libIndexStore: true) + guard let t = Toolchain(binPath) else { + XCTFail("could not find any tools") + return + } + XCTAssertNotNil(t.sourcekitd) + XCTAssertNotNil(t.libIndexStore) } - XCTAssertNotNil(t.sourcekitd) - XCTAssertNotNil(t.libIndexStore) } - func testSubDirs() throws { - let fs = InMemoryFileSystem() - try makeToolchain(binPath: try AbsolutePath(validating: "/t1/bin"), fs, sourcekitd: true) - try makeToolchain(binPath: try AbsolutePath(validating: "/t2/usr/bin"), fs, sourcekitd: true) - - XCTAssertNotNil(Toolchain(try AbsolutePath(validating: "/t1"), fs)) - XCTAssertNotNil(Toolchain(try AbsolutePath(validating: "/t1/bin"), fs)) - XCTAssertNotNil(Toolchain(try AbsolutePath(validating: "/t2"), fs)) - - XCTAssertNil(Toolchain(try AbsolutePath(validating: "/t3"), fs)) - try fs.createDirectory(try AbsolutePath(validating: "/t3/bin"), recursive: true) - try fs.createDirectory(try AbsolutePath(validating: "/t3/lib/sourcekitd.framework"), recursive: true) - XCTAssertNil(Toolchain(try AbsolutePath(validating: "/t3"), fs)) - try makeToolchain(binPath: try AbsolutePath(validating: "/t3/bin"), fs, sourcekitd: true) - XCTAssertNotNil(Toolchain(try AbsolutePath(validating: "/t3"), fs)) + func testSubDirs() async throws { + try await withTestScratchDir { tempDir in + try makeToolchain(binPath: tempDir.appendingPathComponent("t1").appendingPathComponent("bin"), sourcekitd: true) + try makeToolchain( + binPath: tempDir.appendingPathComponent("t2").appendingPathComponent("usr").appendingPathComponent("bin"), + sourcekitd: true + ) + + XCTAssertNotNil(Toolchain(tempDir.appendingPathComponent("t1"))) + XCTAssertNotNil(Toolchain(tempDir.appendingPathComponent("t1").appendingPathComponent("bin"))) + XCTAssertNotNil(Toolchain(tempDir.appendingPathComponent("t2"))) + + XCTAssertNil(Toolchain(tempDir.appendingPathComponent("t3"))) + try FileManager.default.createDirectory( + at: tempDir.appendingPathComponent("t3").appendingPathComponent("bin"), + withIntermediateDirectories: true + ) + try FileManager.default.createDirectory( + at: tempDir.appendingPathComponent("t3").appendingPathComponent("lib").appendingPathComponent( + "sourcekitd.framework" + ), + withIntermediateDirectories: true + ) + XCTAssertNil(Toolchain(tempDir.appendingPathComponent("t3"))) + try makeToolchain(binPath: tempDir.appendingPathComponent("t3").appendingPathComponent("bin"), sourcekitd: true) + XCTAssertNotNil(Toolchain(tempDir.appendingPathComponent("t3"))) + } } func testDuplicateToolchainOnlyRegisteredOnce() async throws { @@ -450,7 +525,7 @@ final class ToolchainRegistryTests: XCTestCase { } func testDuplicatePathOnlyRegisteredOnce() async throws { - let path = try AbsolutePath(validating: "/foo/bar") + let path = URL(fileURLWithPath: "/foo/bar") let first = Toolchain(identifier: "a", displayName: "a", path: path) let second = Toolchain(identifier: "b", displayName: "b", path: path) @@ -459,13 +534,13 @@ final class ToolchainRegistryTests: XCTestCase { } func testMultipleXcodes() async throws { - let pathA = try AbsolutePath(validating: "/versionA") + let pathA = URL(fileURLWithPath: "/versionA") let xcodeA = Toolchain( identifier: ToolchainRegistry.darwinDefaultToolchainIdentifier, displayName: "a", path: pathA ) - let pathB = try AbsolutePath(validating: "/versionB") + let pathB = URL(fileURLWithPath: "/versionB") let xcodeB = Toolchain( identifier: ToolchainRegistry.darwinDefaultToolchainIdentifier, displayName: "b", @@ -485,63 +560,97 @@ final class ToolchainRegistryTests: XCTestCase { } func testInstallPath() async throws { - let fs = InMemoryFileSystem() - try makeToolchain(binPath: try AbsolutePath(validating: "/t1/bin"), fs, sourcekitd: true) - - let trEmpty = ToolchainRegistry(installPath: nil, fs) - await assertNil(trEmpty.default) - - let tr1 = ToolchainRegistry(installPath: try AbsolutePath(validating: "/t1/bin"), fs) - await assertEqual(tr1.default?.path, try AbsolutePath(validating: "/t1/bin")) - await assertNotNil(tr1.default?.sourcekitd) - - let tr2 = ToolchainRegistry(installPath: try AbsolutePath(validating: "/t2/bin"), fs) - await assertNil(tr2.default) + try await withTestScratchDir { tempDir in + let binPath = tempDir.appendingPathComponent("t1").appendingPathComponent("bin", isDirectory: true) + try makeToolchain(binPath: binPath, sourcekitd: true) + + let trEmpty = ToolchainRegistry( + installPath: nil, + environmentVariables: [], + xcodes: [], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) + await assertNil(trEmpty.default) + + let tr1 = ToolchainRegistry( + installPath: binPath, + environmentVariables: [], + xcodes: [], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) + await assertEqual(tr1.default?.path, binPath) + await assertNotNil(tr1.default?.sourcekitd) + + let tr2 = ToolchainRegistry( + installPath: tempDir.appendingPathComponent("t2").appendingPathComponent("bin", isDirectory: true), + environmentVariables: [], + xcodes: [], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) + await assertNil(tr2.default) + } } func testInstallPathVsEnv() async throws { - let fs = InMemoryFileSystem() - try makeToolchain(binPath: try AbsolutePath(validating: "/t1/bin"), fs, sourcekitd: true) - try makeToolchain(binPath: try AbsolutePath(validating: "/t2/bin"), fs, sourcekitd: true) - - try ProcessEnv.setVar("TEST_SOURCEKIT_TOOLCHAIN_PATH_1", value: "/t2/bin") - - let tr = ToolchainRegistry( - installPath: try AbsolutePath(validating: "/t1/bin"), - environmentVariables: ["TEST_SOURCEKIT_TOOLCHAIN_PATH_1"], - fs - ) - await assertEqual(tr.toolchains.count, 2) + try await withTestScratchDir { tempDir in + let t1Bin = tempDir.appendingPathComponent("t1").appendingPathComponent("bin", isDirectory: true) + let t2Bin = tempDir.appendingPathComponent("t2").appendingPathComponent("bin", isDirectory: true) + try makeToolchain(binPath: t1Bin, sourcekitd: true) + try makeToolchain(binPath: t2Bin, sourcekitd: true) + + try ProcessEnv.setVar("TEST_SOURCEKIT_TOOLCHAIN_PATH_1", value: t2Bin.filePath) + + let tr = ToolchainRegistry( + installPath: t1Bin, + environmentVariables: ["TEST_SOURCEKIT_TOOLCHAIN_PATH_1"], + xcodes: [], + libraryDirectories: [], + pathEnvironmentVariables: [], + darwinToolchainOverride: nil + ) + await assertEqual(tr.toolchains.count, 2) - // Env variable wins. - await assertEqual(tr.default?.path, try AbsolutePath(validating: "/t2/bin")) + // Env variable wins. + await assertEqual(tr.default?.path, t2Bin) + } } func testSupersetToolchains() async throws { - let onlySwiftcToolchain = Toolchain( - identifier: "onlySwiftc", - displayName: "onlySwiftc", - path: try AbsolutePath(validating: "/usr/local"), - swiftc: try AbsolutePath(validating: "/usr/local/bin/swiftc") - ) - let swiftcAndSourcekitdToolchain = Toolchain( - identifier: "swiftcAndSourcekitd", - displayName: "swiftcAndSourcekitd", - path: try AbsolutePath(validating: "/usr"), - swiftc: try AbsolutePath(validating: "/usr/bin/swiftc"), - sourcekitd: try AbsolutePath(validating: "/usr/lib/sourcekitd.framework/sourcekitd") - ) + try await withTestScratchDir { tempDir in + let usrLocal = tempDir.appendingPathComponent("usr").appendingPathComponent("local") + let usr = tempDir.appendingPathComponent("usr") + + let onlySwiftcToolchain = Toolchain( + identifier: "onlySwiftc", + displayName: "onlySwiftc", + path: usrLocal, + swiftc: usrLocal.appendingPathComponent("bin").appendingPathComponent("swiftc") + ) + let swiftcAndSourcekitdToolchain = Toolchain( + identifier: "swiftcAndSourcekitd", + displayName: "swiftcAndSourcekitd", + path: usr, + swiftc: usr.appendingPathComponent("bin").appendingPathComponent("swiftc"), + sourcekitd: usrLocal.appendingPathComponent("lib").appendingPathComponent("sourcekitd.framework") + .appendingPathComponent("sourcekitd") + ) - let tr = ToolchainRegistry(toolchains: [onlySwiftcToolchain, swiftcAndSourcekitdToolchain]) - await assertEqual(tr.default?.identifier, "swiftcAndSourcekitd") + let tr = ToolchainRegistry(toolchains: [onlySwiftcToolchain, swiftcAndSourcekitdToolchain]) + await assertEqual(tr.default?.identifier, "swiftcAndSourcekitd") + } } } private func makeXCToolchain( identifier: String, opensource: Bool, - path: AbsolutePath, - _ fs: FileSystem, + path: URL, clang: Bool = false, clangd: Bool = false, swiftc: Bool = false, @@ -550,21 +659,15 @@ private func makeXCToolchain( sourcekitdInProc: Bool = false, libIndexStore: Bool = false ) throws { - try fs.createDirectory(path, recursive: true) - let infoPlistPath = path.appending(component: opensource ? "Info.plist" : "ToolchainInfo.plist") + try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true) + let infoPlistPath = path.appendingPathComponent(opensource ? "Info.plist" : "ToolchainInfo.plist") let infoPlist = try PropertyListEncoder().encode( XCToolchainPlist(identifier: identifier, displayName: "name-\(identifier)") ) - try fs.writeFileContents( - infoPlistPath, - body: { stream in - stream.write(infoPlist) - } - ) + try infoPlist.write(to: infoPlistPath) try makeToolchain( - binPath: path.appending(components: "usr", "bin"), - fs, + binPath: path.appendingPathComponent("usr").appendingPathComponent("bin"), clang: clang, clangd: clangd, swiftc: swiftc, @@ -576,8 +679,7 @@ private func makeXCToolchain( } private func makeToolchain( - binPath: AbsolutePath, - _ fs: FileSystem, + binPath: URL, clang: Bool = false, clangd: Bool = false, swiftc: Bool = false, @@ -586,11 +688,6 @@ private func makeToolchain( sourcekitdInProc: Bool = false, libIndexStore: Bool = false ) throws { - precondition( - !clang && !swiftc && !clangd || !shouldChmod, - "Cannot make toolchain binaries exectuable with InMemoryFileSystem" - ) - // tiny PE binary from: https://archive.is/w01DO let contents: [UInt8] = [ 0x4d, 0x5a, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, 0x4c, 0x01, 0x01, 0x00, @@ -604,15 +701,15 @@ private func makeToolchain( 0x02, ] - let libPath = binPath.parentDirectory.appending(component: "lib") - try fs.createDirectory(binPath, recursive: true) - try fs.createDirectory(libPath) + let libPath = binPath.deletingLastPathComponent().appendingPathComponent("lib") + try FileManager.default.createDirectory(at: binPath, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: libPath, withIntermediateDirectories: true) - let makeExec = { (path: AbsolutePath) in - try fs.writeFileContents(path, bytes: ByteString(contents)) + let makeExec = { (path: URL) in + try Data(contents).write(to: path) #if !os(Windows) if shouldChmod { - XCTAssertEqual(chmod(path.pathString, S_IRUSR | S_IXUSR), 0) + XCTAssertEqual(chmod(try path.filePath, S_IRUSR | S_IXUSR), 0) } #endif } @@ -620,34 +717,37 @@ private func makeToolchain( let execExt = Platform.current?.executableExtension ?? "" if clang { - try makeExec(binPath.appending(component: "clang\(execExt)")) + try makeExec(binPath.appendingPathComponent("clang\(execExt)")) } if clangd { - try makeExec(binPath.appending(component: "clangd\(execExt)")) + try makeExec(binPath.appendingPathComponent("clangd\(execExt)")) } if swiftc { - try makeExec(binPath.appending(component: "swiftc\(execExt)")) + try makeExec(binPath.appendingPathComponent("swiftc\(execExt)")) } let dylibSuffix = Platform.current?.dynamicLibraryExtension ?? ".so" if sourcekitd { - try fs.createDirectory(libPath.appending(component: "sourcekitd.framework")) - try fs.writeFileContents(libPath.appending(components: "sourcekitd.framework", "sourcekitd"), bytes: "") + try FileManager.default.createDirectory( + at: libPath.appendingPathComponent("sourcekitd.framework"), + withIntermediateDirectories: true + ) + try Data().write(to: libPath.appendingPathComponent("sourcekitd.framework").appendingPathComponent("sourcekitd")) } if sourcekitdInProc { #if os(Windows) - try fs.writeFileContents(binPath.appending(component: "sourcekitdInProc\(dylibSuffix)"), bytes: "") + try Data().write(to: binPath.appendingPathComponent("sourcekitdInProc\(dylibSuffix)")) #else - try fs.writeFileContents(libPath.appending(component: "libsourcekitdInProc\(dylibSuffix)"), bytes: "") + try Data().write(to: libPath.appendingPathComponent("libsourcekitdInProc\(dylibSuffix)")) #endif } if libIndexStore { #if os(Windows) // Windows has a prefix of `lib` on this particular library ... - try fs.writeFileContents(binPath.appending(component: "libIndexStore\(dylibSuffix)"), bytes: "") + try Data().write(to: binPath.appendingPathComponent("libIndexStore\(dylibSuffix)")) #else - try fs.writeFileContents(libPath.appending(component: "libIndexStore\(dylibSuffix)"), bytes: "") + try Data().write(to: libPath.appendingPathComponent("libIndexStore\(dylibSuffix)")) #endif } } From 184fa12389c72379b0803f8dbe44a05fe928a4ca Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 16 Nov 2024 08:13:29 +0900 Subject: [PATCH 03/41] Allow `scratchPath` to be relative paths Interpret it as relative to the project root directory if it's a relative path. --- Documentation/Configuration File.md | 2 +- .../SwiftPMBuildSystem.swift | 2 +- .../SwiftPMBuildSystemTests.swift | 37 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Documentation/Configuration File.md b/Documentation/Configuration File.md index 6fd133ff1..d19dcca04 100644 --- a/Documentation/Configuration File.md +++ b/Documentation/Configuration File.md @@ -15,7 +15,7 @@ The structure of the file is currently not guaranteed to be stable. Options may - `swiftPM`: Dictionary with the following keys, defining options for SwiftPM workspaces - `configuration: "debug"|"release"`: The configuration to build the project for during background indexing and the configuration whose build folder should be used for Swift modules if background indexing is disabled. Equivalent to SwiftPM's `--configuration` option. - - `scratchPath: string`: Build artifacts directory path. If nil, the build system may choose a default value. Equivalent to SwiftPM's `--scratch-path` option. + - `scratchPath: string`: Build artifacts directory path. If nil, the build system may choose a default value. This path can be specified as a relative path, which will be interpreted relative to the project root. Equivalent to SwiftPM's `--scratch-path` option. - `swiftSDKsDirectory: string`: Equivalent to SwiftPM's `--swift-sdks-path` option - `swiftSDK: string`: Equivalent to SwiftPM's `--swift-sdk` option - `triple: string`: Equivalent to SwiftPM's `--triple` option diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 6f58cdfcb..69c993faf 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -324,7 +324,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { if options.backgroundIndexingOrDefault { location.scratchDirectory = AbsolutePath(projectRoot.appending(components: ".build", "index-build")) } else if let scratchDirectory = options.swiftPMOrDefault.scratchPath, - let scratchDirectoryPath = try? AbsolutePath(validating: scratchDirectory) + let scratchDirectoryPath = try? AbsolutePath(validating: scratchDirectory, relativeTo: AbsolutePath(projectRoot)) { location.scratchDirectory = scratchDirectoryPath } diff --git a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift index 4764d84fe..fa4efb0f4 100644 --- a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift @@ -90,6 +90,43 @@ final class SwiftPMBuildSystemTests: XCTestCase { } } + func testRelativeScratchPath() async throws { + try await withTestScratchDir { tempDir in + try localFileSystem.createFiles( + root: tempDir, + files: [ + "pkg/Sources/lib/a.swift": "", + "pkg/Package.swift": """ + // swift-tools-version:4.2 + import PackageDescription + let package = Package( + name: "a", + targets: [.target(name: "lib")] + ) + """, + ] + ) + let packageRoot = tempDir.appending(component: "pkg") + let options = SourceKitLSPOptions( + swiftPM: .init( + scratchPath: "non_default_relative_build_path" + ), + backgroundIndexing: false + ) + let swiftpmBuildSystem = try await SwiftPMBuildSystem( + projectRoot: packageRoot, + toolchainRegistry: .forTesting, + options: options, + connectionToSourceKitLSP: LocalConnection(receiverName: "dummy"), + testHooks: SwiftPMTestHooks() + ) + + let dataPath = await swiftpmBuildSystem.destinationBuildParameters.dataPath + let expectedScratchPath = packageRoot.appending(component: try XCTUnwrap(options.swiftPMOrDefault.scratchPath)) + XCTAssertTrue(AbsolutePath(dataPath).isDescendant(of: expectedScratchPath)) + } + } + func testBasicSwiftArgs() async throws { try await SkipUnless.swiftpmStoresModulesInSubdirectory() try await withTestScratchDir { tempDir in From d8db60e6a8924a7a5b216ac3ceb009854fa508bc Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Mon, 18 Nov 2024 18:21:51 -0800 Subject: [PATCH 04/41] Enable `MemberImportVisibility` in the CMake build Forgot to enable this upcoming feature when I added it to the Package manifest. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e64d49017..d63e0b760 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -add_compile_options("$<$:SHELL:-enable-upcoming-feature InternalImportsByDefault>") +add_compile_options("$<$:SHELL:-enable-upcoming-feature InternalImportsByDefault -enable-upcoming-feature MemberImportVisibility>") find_package(dispatch QUIET) find_package(Foundation QUIET) From 76304db5e252d5573f8903a3fad2128c8e90be81 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 19 Nov 2024 15:47:52 -0800 Subject: [PATCH 05/41] Fix merge conflict https://github.com/swiftlang/sourcekit-lsp/pull/1824 and https://github.com/swiftlang/sourcekit-lsp/pull/1832 raced. --- Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift | 5 ++++- .../SwiftPMBuildSystemTests.swift | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 22987f186..55da3710f 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -315,7 +315,10 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { validating: projectRoot.appendingPathComponent(".build").appendingPathComponent("index-build").filePath ) } else if let scratchDirectory = options.swiftPMOrDefault.scratchPath, - let scratchDirectoryPath = try? AbsolutePath(validating: scratchDirectory, relativeTo: AbsolutePath(projectRoot)) + let scratchDirectoryPath = try? AbsolutePath( + validating: scratchDirectory, + relativeTo: AbsolutePath(validating: projectRoot.filePath) + ) { location.scratchDirectory = scratchDirectoryPath } diff --git a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift index b9989dde4..7d676b7a2 100644 --- a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift @@ -92,7 +92,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { func testRelativeScratchPath() async throws { try await withTestScratchDir { tempDir in - try localFileSystem.createFiles( + try FileManager.default.createFiles( root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", @@ -122,8 +122,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { ) let dataPath = await swiftpmBuildSystem.destinationBuildParameters.dataPath - let expectedScratchPath = packageRoot.appending(component: try XCTUnwrap(options.swiftPMOrDefault.scratchPath)) - XCTAssertTrue(AbsolutePath(dataPath).isDescendant(of: expectedScratchPath)) + let expectedScratchPath = packageRoot.appendingPathComponent(try XCTUnwrap(options.swiftPMOrDefault.scratchPath)) + XCTAssertTrue(dataPath.asURL.isDescendant(of: expectedScratchPath)) } } From bfaee492ba678d1efb76e1959add23fc7337b10f Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Wed, 20 Nov 2024 12:48:38 -0800 Subject: [PATCH 06/41] Minor improvements to `DLHandle.Handle` --- Sources/SourceKitD/dlopen.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Sources/SourceKitD/dlopen.swift b/Sources/SourceKitD/dlopen.swift index fd566a57d..3c402c487 100644 --- a/Sources/SourceKitD/dlopen.swift +++ b/Sources/SourceKitD/dlopen.swift @@ -26,18 +26,17 @@ import Android #endif package final class DLHandle: Sendable { - #if os(Windows) - struct Handle: @unchecked Sendable { + fileprivate struct Handle: @unchecked Sendable { + #if os(Windows) let handle: HMODULE - } - #else - struct Handle: @unchecked Sendable { + #else let handle: UnsafeMutableRawPointer + #endif } - #endif - let rawValue: ThreadSafeBox - init(rawValue: Handle) { + fileprivate let rawValue: ThreadSafeBox + + fileprivate init(rawValue: Handle) { self.rawValue = .init(initialValue: rawValue) } From a2eb7b9b2c5ce666112cdd67626fcd0bc05615e4 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Thu, 21 Nov 2024 16:07:22 +0330 Subject: [PATCH 07/41] Handle on-type formatting requests --- Documentation/Configuration File.md | 2 +- Sources/SKOptions/ExperimentalFeatures.swift | 8 +- .../Clang/ClangLanguageService.swift | 4 + Sources/SourceKitLSP/LanguageService.swift | 1 + Sources/SourceKitLSP/SourceKitLSPServer.swift | 22 ++- .../Swift/DocumentFormatting.swift | 30 ++++- .../Swift/SwiftLanguageService.swift | 6 + .../OnTypeFormattingTests.swift | 127 ++++++++++++++++++ 8 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 Tests/SourceKitLSPTests/OnTypeFormattingTests.swift diff --git a/Documentation/Configuration File.md b/Documentation/Configuration File.md index d19dcca04..48b84eb30 100644 --- a/Documentation/Configuration File.md +++ b/Documentation/Configuration File.md @@ -51,5 +51,5 @@ The structure of the file is currently not guaranteed to be stable. Options may - `noLazy`: Prepare a target without generating object files but do not do lazy type checking and function body skipping - `enabled`: Prepare a target without generating object files and the like - `cancelTextDocumentRequestsOnEditAndClose: bool`: Whether sending a `textDocument/didChange` or `textDocument/didClose` notification for a document should cancel all pending requests for that document. -- `experimentalFeatures: string[]`: Experimental features to enable +- `experimentalFeatures: string[]`: Experimental features to enable. Available features: on-type-formatting - `swiftPublishDiagnosticsDebounceDuration: double`: The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and sending a `PublishDiagnosticsNotification`. diff --git a/Sources/SKOptions/ExperimentalFeatures.swift b/Sources/SKOptions/ExperimentalFeatures.swift index 591f87e03..39f9ae954 100644 --- a/Sources/SKOptions/ExperimentalFeatures.swift +++ b/Sources/SKOptions/ExperimentalFeatures.swift @@ -10,9 +10,9 @@ // //===----------------------------------------------------------------------===// -/// An experimental feature that can be enabled by passing `--experimental-feature` to `sourcekit-lsp` on the command -/// line. The raw value of this feature is how it is named on the command line. +/// An experimental feature that can be enabled by passing `--experimental-feature` +/// to `sourcekit-lsp` on the command line or through the configuration file. +/// The raw value of this feature is how it is named on the command line and in the configuration file. public enum ExperimentalFeature: String, Codable, Sendable, CaseIterable { - /* This is here to silence the errors when the enum doesn't have any cases */ - case exampleCase = "example-case" + case onTypeFormatting = "on-type-formatting" } diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift index 34f744f90..255bd8f39 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift @@ -581,6 +581,10 @@ extension ClangLanguageService { return try await forwardRequestToClangd(req) } + func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? { + return try await forwardRequestToClangd(req) + } + func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? { return try await forwardRequestToClangd(req) } diff --git a/Sources/SourceKitLSP/LanguageService.swift b/Sources/SourceKitLSP/LanguageService.swift index bfcc09b2c..aecc2c552 100644 --- a/Sources/SourceKitLSP/LanguageService.swift +++ b/Sources/SourceKitLSP/LanguageService.swift @@ -210,6 +210,7 @@ package protocol LanguageService: AnyObject, Sendable { func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? func documentRangeFormatting(_ req: DocumentRangeFormattingRequest) async throws -> [TextEdit]? + func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? // MARK: - Rename diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 6cedb6c50..f76805de3 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -720,6 +720,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { await self.handleRequest(for: request, requestHandler: self.documentFormatting) case let request as RequestAndReply: await self.handleRequest(for: request, requestHandler: self.documentRangeFormatting) + case let request as RequestAndReply: + await self.handleRequest(for: request, requestHandler: self.documentOnTypeFormatting) case let request as RequestAndReply: await self.handleRequest(for: request, requestHandler: self.documentSymbolHighlight) case let request as RequestAndReply: @@ -971,7 +973,8 @@ extension SourceKitLSPServer { let result = InitializeResult( capabilities: await self.serverCapabilities( for: req.capabilities, - registry: self.capabilityRegistry! + registry: self.capabilityRegistry!, + options: options ) ) logger.logFullObjectInMultipleLogMessages(header: "Initialize response", AnyRequestType(request: req)) @@ -980,7 +983,8 @@ extension SourceKitLSPServer { func serverCapabilities( for client: ClientCapabilities, - registry: CapabilityRegistry + registry: CapabilityRegistry, + options: SourceKitLSPOptions ) async -> ServerCapabilities { let completionOptions = await registry.clientHasDynamicCompletionRegistration @@ -990,6 +994,11 @@ extension SourceKitLSPServer { triggerCharacters: [".", "("] ) + let onTypeFormattingOptions = + options.hasExperimentalFeature(.onTypeFormatting) + ? DocumentOnTypeFormattingOptions(triggerCharacters: ["\n", "\r\n", "\r", "{", "}", ";", ".", ":", "#"]) + : nil + let foldingRangeOptions = await registry.clientHasDynamicFoldingRangeRegistration ? nil @@ -1039,6 +1048,7 @@ extension SourceKitLSPServer { codeLensProvider: CodeLensOptions(), documentFormattingProvider: .value(DocumentFormattingOptions(workDoneProgress: false)), documentRangeFormattingProvider: .value(DocumentRangeFormattingOptions(workDoneProgress: false)), + documentOnTypeFormattingProvider: onTypeFormattingOptions, renameProvider: .value(RenameOptions(prepareProvider: true)), colorProvider: .bool(true), foldingRangeProvider: foldingRangeOptions, @@ -1537,6 +1547,14 @@ extension SourceKitLSPServer { return try await languageService.documentRangeFormatting(req) } + func documentOnTypeFormatting( + _ req: DocumentOnTypeFormattingRequest, + workspace: Workspace, + languageService: LanguageService + ) async throws -> [TextEdit]? { + return try await languageService.documentOnTypeFormatting(req) + } + func colorPresentation( _ req: ColorPresentationRequest, workspace: Workspace, diff --git a/Sources/SourceKitLSP/Swift/DocumentFormatting.swift b/Sources/SourceKitLSP/Swift/DocumentFormatting.swift index 82e6c6a61..1c42c2cf5 100644 --- a/Sources/SourceKitLSP/Swift/DocumentFormatting.swift +++ b/Sources/SourceKitLSP/Swift/DocumentFormatting.swift @@ -15,6 +15,7 @@ import Foundation package import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging +import SKUtilities import SwiftExtensions import SwiftParser import SwiftSyntax @@ -28,6 +29,7 @@ import Foundation import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging +import SKUtilities import SwiftExtensions import SwiftParser import SwiftSyntax @@ -143,6 +145,7 @@ private func edits(from original: DocumentSnapshot, to edited: String) -> [TextE extension SwiftLanguageService { package func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? { return try await format( + snapshot: documentManager.latestSnapshot(req.textDocument.uri), textDocument: req.textDocument, options: req.options ) @@ -150,19 +153,36 @@ extension SwiftLanguageService { package func documentRangeFormatting(_ req: DocumentRangeFormattingRequest) async throws -> [TextEdit]? { return try await format( + snapshot: documentManager.latestSnapshot(req.textDocument.uri), textDocument: req.textDocument, options: req.options, range: req.range ) } + package func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? { + let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) + guard let line = snapshot.lineTable.line(at: req.position.line) else { + return nil + } + + let lineStartPosition = snapshot.position(of: line.startIndex, fromLine: req.position.line) + let lineEndPosition = snapshot.position(of: line.endIndex, fromLine: req.position.line) + + return try await format( + snapshot: snapshot, + textDocument: req.textDocument, + options: req.options, + range: lineStartPosition..? = nil ) async throws -> [TextEdit]? { - let snapshot = try documentManager.latestSnapshot(textDocument.uri) - guard let swiftFormat else { throw ResponseError.unknown( "Formatting not supported because the toolchain is missing the swift-format executable" @@ -176,9 +196,13 @@ extension SwiftLanguageService { swiftFormatConfiguration(for: textDocument.uri, options: options), ] if let range { + let utf8Range = snapshot.utf8OffsetRange(of: range) + // swift-format takes an inclusive range, but Swift's `Range.upperBound` is exclusive. + // Also make sure `upperBound` does not go less than `lowerBound`. + let utf8UpperBound = max(utf8Range.lowerBound, utf8Range.upperBound - 1) args += [ "--offsets", - "\(snapshot.utf8Offset(of: range.lowerBound)):\(snapshot.utf8Offset(of: range.upperBound))", + "\(utf8Range.lowerBound):\(utf8UpperBound)", ] } let process = TSCBasic.Process(arguments: args) diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift index ca83985b7..fa86f15c9 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift @@ -1226,6 +1226,12 @@ extension DocumentSnapshot { return Position(line: zeroBasedLine, utf16index: utf16Column) } + /// Converts the given `String.Index` to a UTF-16-based line:column position. + func position(of index: String.Index, fromLine: Int = 0) -> Position { + let (line, utf16Column) = lineTable.lineAndUTF16ColumnOf(index, fromLine: fromLine) + return Position(line: line, utf16index: utf16Column) + } + // MARK: Position <-> AbsolutePosition /// Converts the given UTF-8-offset-based `AbsolutePosition` to a UTF-16-based line:column. diff --git a/Tests/SourceKitLSPTests/OnTypeFormattingTests.swift b/Tests/SourceKitLSPTests/OnTypeFormattingTests.swift new file mode 100644 index 000000000..114dcfa8a --- /dev/null +++ b/Tests/SourceKitLSPTests/OnTypeFormattingTests.swift @@ -0,0 +1,127 @@ +//===----------------------------------------------------------------------===// +// +// 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 LanguageServerProtocol +import SKLogging +import SKTestSupport +import SourceKitLSP +import XCTest + +final class OnTypeFormattingTests: XCTestCase { + func testOnlyFormatsSpecifiedLine() async throws { + try await SkipUnless.toolchainContainsSwiftFormat() + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + 1️⃣// do stuff + } + } + """, + uri: uri + ) + + let response = try await testClient.send( + DocumentOnTypeFormattingRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["1️⃣"], + ch: "\n", + options: FormattingOptions(tabSize: 2, insertSpaces: true) + ) + ) + + let edits = try XCTUnwrap(response) + XCTAssertEqual( + edits, + [ + TextEdit(range: Range(positions["1️⃣"]), newText: " ") + ] + ) + } + + func testFormatsFullLineAndDoesNotFormatNextLine() async throws { + try await SkipUnless.toolchainContainsSwiftFormat() + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + func foo() { + 1️⃣if let SomeReallyLongVar = 2️⃣ 3️⃣Some.More.Stuff(), let a = 4️⃣ 5️⃣myfunc() 6️⃣{ + } + } + """, + uri: uri + ) + + let response = try await testClient.send( + DocumentOnTypeFormattingRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["6️⃣"], + ch: "{", + options: FormattingOptions(tabSize: 4, insertSpaces: true) + ) + ) + + let edits = try XCTUnwrap(response) + XCTAssertEqual( + edits, + [ + TextEdit(range: Range(positions["1️⃣"]), newText: " "), + TextEdit(range: positions["2️⃣"].. Date: Fri, 22 Nov 2024 15:33:57 +0100 Subject: [PATCH 08/41] Fix quadratic performance issue in `AsyncQueue` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding an item to `AsyncQueue` was linear in the number of pending queue items, thus adding n items to an `AsyncQueue` before any can execute is in O(n^2). This decision was made intentionally because the primary use case for `AsyncQueue` was to track pending LSP requests, of which we don’t expect to have too many pending requests at any given time. While we can't fix the quadratic performance issue in general, we can resolve the quadratic issue of `AsyncQueue` by making a new task only depend on the last item in the queue, which then transitively depends on all the previous items. `AsyncQueue` are the queues that are most likely to contain many items. Fixes #1725 rdar://137886469 --- .../BuildSystemMessageDependencyTracker.swift | 6 ++++- .../MessageHandlingDependencyTracker.swift | 6 ++++- .../Swift/SyntacticTestIndex.swift | 5 +++- Sources/SwiftExtensions/AsyncQueue.swift | 23 +++++++++++-------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Sources/BuildSystemIntegration/BuildSystemMessageDependencyTracker.swift b/Sources/BuildSystemIntegration/BuildSystemMessageDependencyTracker.swift index 2c9afad72..a79880006 100644 --- a/Sources/BuildSystemIntegration/BuildSystemMessageDependencyTracker.swift +++ b/Sources/BuildSystemIntegration/BuildSystemMessageDependencyTracker.swift @@ -15,7 +15,7 @@ import BuildServerProtocol package import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging -import SwiftExtensions +package import SwiftExtensions #else import BuildServerProtocol import LanguageServerProtocol @@ -82,6 +82,10 @@ package enum BuildSystemMessageDependencyTracker: QueueBasedMessageHandlerDepend } } + package func dependencies(in pendingTasks: [PendingTask]) -> [PendingTask] { + return pendingTasks.filter { $0.metadata.isDependency(of: self) } + } + package init(_ request: some RequestType) { switch request { case is BuildShutdownRequest: diff --git a/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift b/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift index 483ca1d02..3eae58886 100644 --- a/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift +++ b/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift @@ -14,7 +14,7 @@ package import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging -import SwiftExtensions +package import SwiftExtensions #else import LanguageServerProtocol import LanguageServerProtocolExtensions @@ -90,6 +90,10 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc } } + package func dependencies(in pendingTasks: [PendingTask]) -> [PendingTask] { + return pendingTasks.filter { $0.metadata.isDependency(of: self) } + } + package init(_ notification: some NotificationType) { switch notification { case is CancelRequestNotification: diff --git a/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift b/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift index a8c114dc5..d2866fc55 100644 --- a/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift +++ b/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift @@ -37,7 +37,6 @@ fileprivate enum TaskMetadata: DependencyTracker, Equatable { case (.index(_), .read): // We require all index tasks scheduled before the read to be finished. // This ensures that the index has been updated at least to the state of file at which the read was scheduled. - // Adding the dependency also elevates the index task's priorities. return true case (.index(let lhsUris), .index(let rhsUris)): // Technically, we should be able to allow simultaneous indexing of the same file. But conceptually the code @@ -47,6 +46,10 @@ fileprivate enum TaskMetadata: DependencyTracker, Equatable { return !lhsUris.intersection(rhsUris).isEmpty } } + + package func dependencies(in pendingTasks: [PendingTask]) -> [PendingTask] { + return pendingTasks.filter { $0.metadata.isDependency(of: self) } + } } /// Data from a syntactic scan of a source file for tests. diff --git a/Sources/SwiftExtensions/AsyncQueue.swift b/Sources/SwiftExtensions/AsyncQueue.swift index 5bbab093c..943dcbd78 100644 --- a/Sources/SwiftExtensions/AsyncQueue.swift +++ b/Sources/SwiftExtensions/AsyncQueue.swift @@ -28,28 +28,31 @@ extension Task: AnyTask { /// A type that is able to track dependencies between tasks. package protocol DependencyTracker: Sendable { - /// Whether the task described by `self` needs to finish executing before - /// `other` can start executing. - func isDependency(of other: Self) -> Bool + /// Which tasks need to finish before a task described by `self` may start executing. + /// `pendingTasks` is sorted in the order in which the tasks were enqueued to `AsyncQueue`. + func dependencies(in pendingTasks: [PendingTask]) -> [PendingTask] } /// A dependency tracker where each task depends on every other, i.e. a serial /// queue. package struct Serial: DependencyTracker { - package func isDependency(of other: Serial) -> Bool { - return true + package func dependencies(in pendingTasks: [PendingTask]) -> [PendingTask] { + if let lastTask = pendingTasks.last { + return [lastTask] + } + return [] } } -private struct PendingTask: Sendable { +package struct PendingTask: Sendable { /// The task that is pending. - let task: any AnyTask + fileprivate let task: any AnyTask - let metadata: TaskMetadata + package let metadata: TaskMetadata /// A unique value used to identify the task. This allows tasks to get /// removed from `pendingTasks` again after they finished executing. - let id: UUID + fileprivate let id: UUID } /// A list of pending tasks that can be sent across actor boundaries and is guarded by a lock. @@ -132,7 +135,7 @@ package final class AsyncQueue: Sendable { return pendingTasks.withLock { tasks in // Build the list of tasks that need to finished execution before this one // can be executed - let dependencies: [PendingTask] = tasks.filter { $0.metadata.isDependency(of: metadata) } + let dependencies = metadata.dependencies(in: tasks) // Schedule the task. let task = Task(priority: priority) { [pendingTasks] in From ea6f06f9e4423b40d14a84fff04bd7f673eef5cd Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 22 Nov 2024 15:44:59 +0100 Subject: [PATCH 09/41] Log integers and booleans as public information by default in `NonDarwinLogger` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os_log` doesn’t consider integers and bools as private information and neither should `NonDarwinLogger`. rdar://138659073 --- Sources/SKLogging/NonDarwinLogging.swift | 11 +++++++++++ Tests/SKLoggingTests/LoggingTests.swift | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Sources/SKLogging/NonDarwinLogging.swift b/Sources/SKLogging/NonDarwinLogging.swift index fd9c5940a..e156b571c 100644 --- a/Sources/SKLogging/NonDarwinLogging.swift +++ b/Sources/SKLogging/NonDarwinLogging.swift @@ -222,6 +222,17 @@ package struct NonDarwinLogInterpolation: StringInterpolationProtocol, Sendable append(description: String(reflecting: type), redactedDescription: "", privacy: privacy) } + package mutating func appendInterpolation( + _ message: some Numeric & Sendable, + privacy: NonDarwinLogPrivacy = .public + ) { + append(description: String(describing: message), redactedDescription: "", privacy: privacy) + } + + package mutating func appendInterpolation(_ message: Bool, privacy: NonDarwinLogPrivacy = .public) { + append(description: message.description, redactedDescription: "", privacy: privacy) + } + /// Builds the string that represents the log message, masking all interpolation /// segments whose privacy level is greater that `logPrivacyLevel`. fileprivate func string(for logPrivacyLevel: NonDarwinLogPrivacy) -> String { diff --git a/Tests/SKLoggingTests/LoggingTests.swift b/Tests/SKLoggingTests/LoggingTests.swift index 92f571e86..aefbc1cb1 100644 --- a/Tests/SKLoggingTests/LoggingTests.swift +++ b/Tests/SKLoggingTests/LoggingTests.swift @@ -212,4 +212,22 @@ final class LoggingTests: XCTestCase { $0.log("got \(LogStringConvertible().forLogging)") } } + + func testIntegerNotConsideredPrivate() async { + await assertLogging( + privacyLevel: .public, + expected: ["got 42"] + ) { + $0.log("got \(42)") + } + } + + func testBoolNotConsideredPrivate() async { + await assertLogging( + privacyLevel: .public, + expected: ["got true"] + ) { + $0.log("got \(true)") + } + } } From e073a01bbc91bd117d3ee6a054cec5dc794bbca1 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 22 Nov 2024 15:52:27 +0100 Subject: [PATCH 10/41] Reply with `null` to `shutdown` request The LSP spec says the result of `shutdown` is `null`, not an empty object. Fixes #1733 rdar://137886488 --- .../Requests/ShutdownRequest.swift | 15 +++++++++++---- Sources/SourceKitLSP/SourceKitLSPServer.swift | 4 ++-- .../LanguageServerProtocolTests/CodingTests.swift | 4 ++++ Tests/SourceKitLSPTests/IndexTests.swift | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Sources/LanguageServerProtocol/Requests/ShutdownRequest.swift b/Sources/LanguageServerProtocol/Requests/ShutdownRequest.swift index aca90bcfa..036aa020f 100644 --- a/Sources/LanguageServerProtocol/Requests/ShutdownRequest.swift +++ b/Sources/LanguageServerProtocol/Requests/ShutdownRequest.swift @@ -18,10 +18,17 @@ /// - Returns: Void. public struct ShutdownRequest: RequestType, Hashable { public static let method: String = "shutdown" - public typealias Response = VoidResponse + + public struct Response: ResponseType, Equatable { + public init() {} + + public init(from decoder: any Decoder) throws {} + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encodeNil() + } + } public init() {} } - -@available(*, deprecated, renamed: "ShutdownRequest") -public typealias Shutdown = ShutdownRequest diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 6cedb6c50..c2af608e8 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -1128,7 +1128,7 @@ extension SourceKitLSPServer { } } - func shutdown(_ request: ShutdownRequest) async throws -> VoidResponse { + func shutdown(_ request: ShutdownRequest) async throws -> ShutdownRequest.Response { await prepareForExit() await withTaskGroup(of: Void.self) { taskGroup in @@ -1159,7 +1159,7 @@ extension SourceKitLSPServer { // Otherwise we might terminate sourcekit-lsp while it still has open // connections to the toolchain servers, which could send messages to // sourcekit-lsp while it is being deallocated, causing crashes. - return VoidResponse() + return ShutdownRequest.Response() } func exit(_ notification: ExitNotification) async { diff --git a/Tests/LanguageServerProtocolTests/CodingTests.swift b/Tests/LanguageServerProtocolTests/CodingTests.swift index ee81ec2b0..72b0e13e1 100644 --- a/Tests/LanguageServerProtocolTests/CodingTests.swift +++ b/Tests/LanguageServerProtocolTests/CodingTests.swift @@ -1343,6 +1343,10 @@ final class CodingTests: XCTestCase { """ ) } + + func testShutdownResponse() { + checkCoding(ShutdownRequest.Response(), json: "null") + } } func with(_ value: T, mutate: (inout T) -> Void) -> T { diff --git a/Tests/SourceKitLSPTests/IndexTests.swift b/Tests/SourceKitLSPTests/IndexTests.swift index 76450db37..2452e8cf5 100644 --- a/Tests/SourceKitLSPTests/IndexTests.swift +++ b/Tests/SourceKitLSPTests/IndexTests.swift @@ -141,7 +141,7 @@ final class IndexTests: XCTestCase { "Received unexpected version: \(versionContentsBefore.first?.lastPathComponent ?? "")" ) - try await project.testClient.send(ShutdownRequest()) + _ = try await project.testClient.send(ShutdownRequest()) return versionedPath } From 14b858822126f50d54c81e733f81451fce7f9f69 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 22 Nov 2024 15:59:07 +0100 Subject: [PATCH 11/41] =?UTF-8?q?Don=E2=80=99t=20crash=20when=20opening=20?= =?UTF-8?q?a=20file=20with=20an=20empty=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1739 rdar://137886470 --- .../LanguageServerProtocol/SupportTypes/DocumentURI.swift | 8 ++++++-- Tests/SourceKitLSPTests/LifecycleTests.swift | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/LanguageServerProtocol/SupportTypes/DocumentURI.swift b/Sources/LanguageServerProtocol/SupportTypes/DocumentURI.swift index a0016bcd9..5f593d747 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/DocumentURI.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/DocumentURI.swift @@ -56,8 +56,12 @@ public struct DocumentURI: Codable, Hashable, Sendable { /// fallback mode that drops semantic functionality. public var pseudoPath: String { if storage.isFileURL { - return storage.withUnsafeFileSystemRepresentation { - String(cString: $0!) + return storage.withUnsafeFileSystemRepresentation { filePath in + if let filePath { + String(cString: filePath) + } else { + "" + } } } else { return storage.absoluteString diff --git a/Tests/SourceKitLSPTests/LifecycleTests.swift b/Tests/SourceKitLSPTests/LifecycleTests.swift index 956dfdf68..fef386c80 100644 --- a/Tests/SourceKitLSPTests/LifecycleTests.swift +++ b/Tests/SourceKitLSPTests/LifecycleTests.swift @@ -164,4 +164,9 @@ final class LifecycleTests: XCTestCase { ) ) } + + func testOpenFileWithoutPath() async throws { + let testClient = try await TestSourceKitLSPClient() + testClient.openDocument("", uri: DocumentURI(try XCTUnwrap(URL(string: "file://"))), language: .swift) + } } From eb72e10886e6734a409a75674d2d7626f1ece612 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 22 Nov 2024 21:14:46 +0100 Subject: [PATCH 12/41] Fully qualify type names in call hierarchy, type hierarchy and workspace symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we didn’t take outer types into account or only took one level of container type into account. Fixes #1673 rdar://136078089 --- Sources/SourceKitLSP/SourceKitLSPServer.swift | 226 +++++++++--------- .../CallHierarchyTests.swift | 2 +- .../TypeHierarchyTests.swift | 42 ++++ 3 files changed, 156 insertions(+), 114 deletions(-) diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 6cedb6c50..d362f471c 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -1444,13 +1444,24 @@ extension SourceKitLSPServer { range: Range(symbolPosition) ) + let containerNames = index.containerNames(of: symbolOccurrence) + let containerName: String? + if containerNames.isEmpty { + containerName = nil + } else { + switch symbolOccurrence.symbol.language { + case .cxx, .c, .objc: containerName = containerNames.joined(separator: "::") + case .swift: containerName = containerNames.joined(separator: ".") + } + } + return WorkspaceSymbolItem.symbolInformation( SymbolInformation( name: symbolOccurrence.symbol.name, kind: symbolOccurrence.symbol.kind.asLspSymbolKind(), deprecated: nil, location: symbolLocation, - containerName: index.containerName(of: symbolOccurrence) + containerName: containerName ) ) } @@ -1916,27 +1927,14 @@ extension SourceKitLSPServer { } private func indexToLSPCallHierarchyItem( - symbol: Symbol, - containerName: String?, - location: Location - ) -> CallHierarchyItem { - let name: String - if let containerName { - switch symbol.language { - case .objc where symbol.kind == .instanceMethod || symbol.kind == .instanceProperty: - name = "-[\(containerName) \(symbol.name)]" - case .objc where symbol.kind == .classMethod || symbol.kind == .classProperty: - name = "+[\(containerName) \(symbol.name)]" - case .cxx, .c, .objc: - // C shouldn't have container names for call hierarchy and Objective-C should be covered above. - // Fall back to using the C++ notation using `::`. - name = "\(containerName)::\(symbol.name)" - case .swift: - name = "\(containerName).\(symbol.name)" - } - } else { - name = symbol.name + definition: SymbolOccurrence, + index: CheckedIndex + ) -> CallHierarchyItem? { + guard let location = indexToLSPLocation(definition.location) else { + return nil } + let name = index.fullyQualifiedName(of: definition) + let symbol = definition.symbol return CallHierarchyItem( name: name, kind: symbol.kind.asLspSymbolKind(), @@ -1976,14 +1974,7 @@ extension SourceKitLSPServer { guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else { return nil } - guard let location = indexToLSPLocation(definition.location) else { - return nil - } - return self.indexToLSPCallHierarchyItem( - symbol: definition.symbol, - containerName: index.containerName(of: definition), - location: location - ) + return self.indexToLSPCallHierarchyItem(definition: definition, index: index) }.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, @@ -2045,38 +2036,27 @@ extension SourceKitLSPServer { // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPCallHierarchyItem2( - symbol: Symbol, - containerName: String?, - location: Location - ) -> CallHierarchyItem { - return self.indexToLSPCallHierarchyItem(symbol: symbol, containerName: containerName, location: location) + definition: SymbolOccurrence, + index: CheckedIndex + ) -> CallHierarchyItem? { + return self.indexToLSPCallHierarchyItem(definition: definition, index: index) } let calls = callersToCalls.compactMap { (caller: Symbol, calls: [SymbolOccurrence]) -> CallHierarchyIncomingCall? in // Resolve the caller's definition to find its location - let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: caller.usr) - let definitionSymbolLocation = definition?.location - let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation2) - let containerName: String? = - if let definition { - index.containerName(of: definition) - } else { - nil - } + guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: caller.usr) else { + return nil + } let locations = calls.compactMap { indexToLSPLocation2($0.location) }.sorted() guard !locations.isEmpty else { return nil } + guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else { + return nil + } - return CallHierarchyIncomingCall( - from: indexToLSPCallHierarchyItem2( - symbol: caller, - containerName: containerName, - location: definitionLocation ?? locations.first! - ), - fromRanges: locations.map(\.range) - ) + return CallHierarchyIncomingCall(from: item, fromRanges: locations.map(\.range)) } return calls.sorted(by: { $0.from.name < $1.from.name }) } @@ -2095,11 +2075,10 @@ extension SourceKitLSPServer { // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPCallHierarchyItem2( - symbol: Symbol, - containerName: String?, - location: Location - ) -> CallHierarchyItem { - return self.indexToLSPCallHierarchyItem(symbol: symbol, containerName: containerName, location: location) + definition: SymbolOccurrence, + index: CheckedIndex + ) -> CallHierarchyItem? { + return self.indexToLSPCallHierarchyItem(definition: definition, index: index) } let callableUsrs = [data.usr] + index.occurrences(relatedToUSR: data.usr, roles: .accessorOf).map(\.symbol.usr) @@ -2113,37 +2092,32 @@ extension SourceKitLSPServer { } // Resolve the callee's definition to find its location - let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) - let definitionSymbolLocation = definition?.location - let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation2) - let containerName: String? = - if let definition { - index.containerName(of: definition) - } else { - nil - } + guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) else { + return nil + } - return CallHierarchyOutgoingCall( - to: indexToLSPCallHierarchyItem2( - symbol: occurrence.symbol, - containerName: containerName, - location: definitionLocation ?? location // Use occurrence location as fallback - ), - fromRanges: [location.range] - ) + guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else { + return nil + } + + return CallHierarchyOutgoingCall(to: item, fromRanges: [location.range]) } return calls.sorted(by: { $0.to.name < $1.to.name }) } private func indexToLSPTypeHierarchyItem( - symbol: Symbol, + definition: SymbolOccurrence, moduleName: String?, - location: Location, index: CheckedIndex - ) -> TypeHierarchyItem { + ) -> TypeHierarchyItem? { let name: String let detail: String? + guard let location = indexToLSPLocation(definition.location) else { + return nil + } + + let symbol = definition.symbol switch symbol.kind { case .extension: // Query the conformance added by this extension @@ -2164,7 +2138,7 @@ extension SourceKitLSPServer { detail = "Extension" } default: - name = symbol.name + name = index.fullyQualifiedName(of: definition) detail = moduleName } @@ -2213,16 +2187,12 @@ extension SourceKitLSPServer { } .compactMap(\.usr) let typeHierarchyItems = usrs.compactMap { (usr) -> TypeHierarchyItem? in - guard - let info = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr), - let location = indexToLSPLocation(info.location) - else { + guard let info = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else { return nil } return self.indexToLSPTypeHierarchyItem( - symbol: info.symbol, + definition: info, moduleName: info.location.moduleName, - location: location, index: index ) } @@ -2287,30 +2257,28 @@ extension SourceKitLSPServer { // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPTypeHierarchyItem2( - symbol: Symbol, + definition: SymbolOccurrence, moduleName: String?, - location: Location, index: CheckedIndex - ) -> TypeHierarchyItem { - return self.indexToLSPTypeHierarchyItem(symbol: symbol, moduleName: moduleName, location: location, index: index) + ) -> TypeHierarchyItem? { + return self.indexToLSPTypeHierarchyItem( + definition: definition, + moduleName: moduleName, + index: index + ) } // Convert occurrences to type hierarchy items let occurs = baseOccurs + retroactiveConformanceOccurs let types = occurs.compactMap { occurrence -> TypeHierarchyItem? in - guard let location = indexToLSPLocation2(occurrence.location) else { + // Resolve the supertype's definition to find its location + guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) else { return nil } - // Resolve the supertype's definition to find its location - let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) - let definitionSymbolLocation = definition?.location - let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation2) - return indexToLSPTypeHierarchyItem2( - symbol: occurrence.symbol, - moduleName: definitionSymbolLocation?.moduleName, - location: definitionLocation ?? location, // Use occurrence location as fallback + definition: definition, + moduleName: definition.location.moduleName, index: index ) } @@ -2334,12 +2302,15 @@ extension SourceKitLSPServer { // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPTypeHierarchyItem2( - symbol: Symbol, + definition: SymbolOccurrence, moduleName: String?, - location: Location, index: CheckedIndex - ) -> TypeHierarchyItem { - return self.indexToLSPTypeHierarchyItem(symbol: symbol, moduleName: moduleName, location: location, index: index) + ) -> TypeHierarchyItem? { + return self.indexToLSPTypeHierarchyItem( + definition: definition, + moduleName: moduleName, + index: index + ) } // Convert occurrences to type hierarchy items @@ -2350,20 +2321,18 @@ extension SourceKitLSPServer { // to. logger.fault("Expected at most extendedBy or baseOf relation but got \(occurrence.relations.count)") } - guard let related = occurrence.relations.sorted().first, let location = indexToLSPLocation2(occurrence.location) - else { + guard let related = occurrence.relations.sorted().first else { return nil } // Resolve the subtype's definition to find its location - let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: related.symbol.usr) - let definitionSymbolLocation = definition.map(\.location) - let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation2) + guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: related.symbol.usr) else { + return nil + } return indexToLSPTypeHierarchyItem2( - symbol: related.symbol, - moduleName: definitionSymbolLocation?.moduleName, - location: definitionLocation ?? location, // Use occurrence location as fallback + definition: definition, + moduleName: definition.location.moduleName, index: index ) } @@ -2408,8 +2377,7 @@ private let maxWorkspaceSymbolResults = 4096 package typealias Diagnostic = LanguageServerProtocol.Diagnostic fileprivate extension CheckedIndex { - /// Get the name of the symbol that is a parent of this symbol, if one exists - func containerName(of symbol: SymbolOccurrence) -> String? { + func containerNames(of symbol: SymbolOccurrence) -> [String] { // The container name of accessors is the container of the surrounding variable. let accessorOf = symbol.relations.filter { $0.roles.contains(.accessorOf) } if let primaryVariable = accessorOf.sorted().first { @@ -2417,7 +2385,7 @@ fileprivate extension CheckedIndex { logger.fault("Expected an occurrence to an accessor of at most one symbol, not multiple") } if let primaryVariable = primaryDefinitionOrDeclarationOccurrence(ofUSR: primaryVariable.symbol.usr) { - return containerName(of: primaryVariable) + return containerNames(of: primaryVariable) } } @@ -2425,7 +2393,7 @@ fileprivate extension CheckedIndex { if containers.count > 1 { logger.fault("Expected an occurrence to a child of at most one symbol, not multiple") } - return containers.filter { + let container = containers.filter { switch $0.symbol.kind { case .module, .namespace, .enum, .struct, .class, .protocol, .extension, .union: return true @@ -2434,7 +2402,39 @@ fileprivate extension CheckedIndex { .destructor, .conversionFunction, .parameter, .using, .concept, .commentTag: return false } - }.sorted().first?.symbol.name + }.sorted().first + + if let container { + if let containerDefinition = primaryDefinitionOrDeclarationOccurrence(ofUSR: container.symbol.usr) { + return self.containerNames(of: containerDefinition) + [container.symbol.name] + } + return [container.symbol.name] + } else { + return [] + } + } + + /// Take the name of containers into account to form a fully-qualified name for the given symbol. + /// This means that we will form names of nested types and type-qualify methods. + func fullyQualifiedName(of symbolOccurrence: SymbolOccurrence) -> String { + let symbol = symbolOccurrence.symbol + let containerNames = containerNames(of: symbolOccurrence) + guard let containerName = containerNames.last else { + // No containers, so nothing to do. + return symbol.name + } + switch symbol.language { + case .objc where symbol.kind == .instanceMethod || symbol.kind == .instanceProperty: + return "-[\(containerName) \(symbol.name)]" + case .objc where symbol.kind == .classMethod || symbol.kind == .classProperty: + return "+[\(containerName) \(symbol.name)]" + case .cxx, .c, .objc: + // C shouldn't have container names for call hierarchy and Objective-C should be covered above. + // Fall back to using the C++ notation using `::`. + return (containerNames + [symbol.name]).joined(separator: "::") + case .swift: + return (containerNames + [symbol.name]).joined(separator: ".") + } } } diff --git a/Tests/SourceKitLSPTests/CallHierarchyTests.swift b/Tests/SourceKitLSPTests/CallHierarchyTests.swift index 21b24572b..7919a0ea7 100644 --- a/Tests/SourceKitLSPTests/CallHierarchyTests.swift +++ b/Tests/SourceKitLSPTests/CallHierarchyTests.swift @@ -833,7 +833,7 @@ final class CallHierarchyTests: XCTestCase { [ CallHierarchyIncomingCall( from: CallHierarchyItem( - name: "Bar.init()", + name: "Outer.Bar.init()", kind: .constructor, tags: nil, uri: project.fileURI, diff --git a/Tests/SourceKitLSPTests/TypeHierarchyTests.swift b/Tests/SourceKitLSPTests/TypeHierarchyTests.swift index a1ed51a7b..771c80abe 100644 --- a/Tests/SourceKitLSPTests/TypeHierarchyTests.swift +++ b/Tests/SourceKitLSPTests/TypeHierarchyTests.swift @@ -206,6 +206,48 @@ final class TypeHierarchyTests: XCTestCase { ) XCTAssertNil(response) } + + func testNestedTypeNameInSubtypes() async throws { + let project = try await IndexedSingleSwiftFileTestProject( + """ + protocol 1️⃣MyProto {} + struct Outer { + struct 2️⃣Mid: MyProto { + struct 3️⃣Inner: MyProto {} + } + } + """ + ) + let item = try await project.prepareTypeHierarchy(at: "1️⃣") + let subtypes = try await project.testClient.send(TypeHierarchySubtypesRequest(item: item)) + assertEqualIgnoringData( + subtypes, + [ + TypeHierarchyItem(name: "Outer.Mid", kind: .struct, location: "2️⃣", in: project), + TypeHierarchyItem(name: "Outer.Mid.Inner", kind: .struct, location: "3️⃣", in: project), + ] + ) + } + + func testNestedTypeNameInSupertypes() async throws { + let project = try await IndexedSingleSwiftFileTestProject( + """ + class Outer { + class 1️⃣Mid { + class 2️⃣Inner: Mid {} + } + } + """ + ) + let item = try await project.prepareTypeHierarchy(at: "2️⃣") + let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item)) + assertEqualIgnoringData( + supertypes, + [ + TypeHierarchyItem(name: "Outer.Mid", kind: .class, location: "1️⃣", in: project) + ] + ) + } } // MARK: - Utilities From cddd8104553466ca44a4b5e96642579f509b889d Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 22 Nov 2024 22:03:56 +0100 Subject: [PATCH 13/41] Provide build settings for version-specific package manifests Fixes #1670 rdar://136014520 --- .../SwiftPMBuildSystem.swift | 27 ++++++++--- .../SwiftPMBuildSystemTests.swift | 45 +++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 55da3710f..e789d1c2e 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -537,16 +537,29 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { // (https://github.com/swiftlang/sourcekit-lsp/issues/1267) for target in request.targets { if target == .forPackageManifest { + let packageManifestName = #/^Package@swift-(\d+)(?:\.(\d+))?(?:\.(\d+))?.swift$/# + let versionSpecificManifests = try? FileManager.default.contentsOfDirectory( + at: projectRoot, + includingPropertiesForKeys: nil + ).compactMap { (url) -> SourceItem? in + guard (try? packageManifestName.wholeMatch(in: url.lastPathComponent)) != nil else { + return nil + } + return SourceItem( + uri: DocumentURI(url), + kind: .file, + generated: false + ) + } + let packageManifest = SourceItem( + uri: DocumentURI(projectRoot.appendingPathComponent("Package.swift")), + kind: .file, + generated: false + ) result.append( SourcesItem( target: target, - sources: [ - SourceItem( - uri: DocumentURI(projectRoot.appendingPathComponent("Package.swift")), - kind: .file, - generated: false - ) - ] + sources: [packageManifest] + (versionSpecificManifests ?? []) ) ) } diff --git a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift index 7d676b7a2..339ea64f0 100644 --- a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift @@ -1103,6 +1103,51 @@ final class SwiftPMBuildSystemTests: XCTestCase { XCTAssertEqual(end.token, begin.token) XCTAssertEqual(end.value, .end(WorkDoneProgressEnd())) } + + func testBuildSettingsForVersionSpecificPackageManifest() async throws { + try await withTestScratchDir { tempDir in + try FileManager.default.createFiles( + root: tempDir, + files: [ + "pkg/Sources/lib/a.swift": "", + "pkg/Package.swift": """ + // swift-tools-version:4.2 + import PackageDescription + let package = Package( + name: "a", + targets: [.target(name: "lib")] + ) + """, + "pkg/Package@swift-5.8.swift": """ + // swift-tools-version:4.2 + import PackageDescription + let package = Package( + name: "a", + targets: [.target(name: "lib")] + ) + """, + ] + ) + let packageRoot = try tempDir.appendingPathComponent("pkg").realpath + let versionSpecificManifestURL = packageRoot.appendingPathComponent("Package@swift-5.8.swift") + let buildSystemManager = await BuildSystemManager( + buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), + toolchainRegistry: .forTesting, + options: SourceKitLSPOptions(), + connectionToClient: DummyBuildSystemManagerConnectionToClient(), + buildSystemTestHooks: BuildSystemTestHooks() + ) + await buildSystemManager.waitForUpToDateBuildGraph() + let settings = await buildSystemManager.buildSettingsInferredFromMainFile( + for: DocumentURI(versionSpecificManifestURL), + language: .swift, + fallbackAfterTimeout: false + ) + let compilerArgs = try XCTUnwrap(settings?.compilerArguments) + XCTAssert(compilerArgs.contains("-package-description-version")) + XCTAssert(compilerArgs.contains(try versionSpecificManifestURL.filePath)) + } + } } private func assertArgumentsDoNotContain( From 62f005cb38447b705d2d9d1ccf3d2d778dbfefef Mon Sep 17 00:00:00 2001 From: Finagolfin Date: Sat, 30 Nov 2024 11:59:29 +0530 Subject: [PATCH 14/41] Add another import of new Android overlay and remove non-existent file exclusion from package manifest --- Package.swift | 1 - Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index aa13fdd92..2d87941e6 100644 --- a/Package.swift +++ b/Package.swift @@ -487,7 +487,6 @@ var targets: [Target] = [ "SwiftExtensions", "TSCExtensions", ], - exclude: ["CMakeLists.txt"], swiftSettings: globalSwiftSettings ), ] diff --git a/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift b/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift index 0be22ce87..33ca7ee51 100644 --- a/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift +++ b/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift @@ -16,6 +16,9 @@ public import Foundation public import LanguageServerProtocol import SKLogging import SwiftExtensions +#if canImport(Android) +import Android +#endif #else import Dispatch import Foundation From e938dfaa7d2cad59b48ec48dee5b72c81cda7c47 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 7 Nov 2024 14:51:49 -0500 Subject: [PATCH 15/41] include parameters in initializer document symbol --- .../SourceKitLSP/Swift/DocumentSymbols.swift | 14 +++++++- .../DocumentSymbolTests.swift | 34 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/DocumentSymbols.swift b/Sources/SourceKitLSP/Swift/DocumentSymbols.swift index 7d2acbc50..abdbd773f 100644 --- a/Sources/SourceKitLSP/Swift/DocumentSymbols.swift +++ b/Sources/SourceKitLSP/Swift/DocumentSymbols.swift @@ -247,7 +247,7 @@ fileprivate final class DocumentSymbolsFinder: SyntaxAnyVisitor { override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { return record( node: node, - name: node.initKeyword.text, + name: node.declName, symbolKind: .constructor, range: node.rangeWithoutTrivia, selection: node.initKeyword @@ -304,6 +304,18 @@ fileprivate extension FunctionDeclSyntax { } } +fileprivate extension InitializerDeclSyntax { + var declName: String { + var result = self.initKeyword.text + result += "(" + for parameter in self.signature.parameterClause.parameters { + result += "\(parameter.firstName.text):" + } + result += ")" + return result + } +} + fileprivate extension SyntaxProtocol { /// The position range of this node without its leading and trailing trivia. var rangeWithoutTrivia: Range { diff --git a/Tests/SourceKitLSPTests/DocumentSymbolTests.swift b/Tests/SourceKitLSPTests/DocumentSymbolTests.swift index ec39e4bb9..9de912025 100644 --- a/Tests/SourceKitLSPTests/DocumentSymbolTests.swift +++ b/Tests/SourceKitLSPTests/DocumentSymbolTests.swift @@ -552,7 +552,39 @@ final class DocumentSymbolTests: XCTestCase { selectionRange: positions["2️⃣"].. Date: Thu, 5 Dec 2024 09:53:42 -0500 Subject: [PATCH 16/41] include all resources in a build target's sources list --- .../SwiftPMBuildSystem.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 55da3710f..193f5931f 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -565,6 +565,27 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { data: SourceKitSourceItemData(isHeader: true).encodeToLSPAny() ) } + sources += swiftPMTarget.resources.map { + SourceItem( + uri: DocumentURI($0), + kind: $0.isDirectory ? .directory : .file, + generated: false, + ) + } + sources += swiftPMTarget.ignored.map { + SourceItem( + uri: DocumentURI($0), + kind: $0.isDirectory ? .directory : .file, + generated: false, + ) + } + sources += swiftPMTarget.others.map { + SourceItem( + uri: DocumentURI($0), + kind: $0.isDirectory ? .directory : .file, + generated: false, + ) + } result.append(SourcesItem(target: target, sources: sources)) } return BuildTargetSourcesResponse(items: result) @@ -760,3 +781,9 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { return TextDocumentSourceKitOptionsResponse(compilerArguments: compilerArgs) } } + +fileprivate extension URL { + var isDirectory: Bool { + (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true + } +} From f2fed7afa5e3e9efa33debd06050b41fc0c827bd Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 5 Dec 2024 10:48:56 -0500 Subject: [PATCH 17/41] reduce code duplication Co-authored-by: Alex Hoppen --- .../SwiftPMBuildSystem.swift | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 193f5931f..27466def7 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -565,21 +565,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { data: SourceKitSourceItemData(isHeader: true).encodeToLSPAny() ) } - sources += swiftPMTarget.resources.map { - SourceItem( - uri: DocumentURI($0), - kind: $0.isDirectory ? .directory : .file, - generated: false, - ) - } - sources += swiftPMTarget.ignored.map { - SourceItem( - uri: DocumentURI($0), - kind: $0.isDirectory ? .directory : .file, - generated: false, - ) - } - sources += swiftPMTarget.others.map { + sources += (swiftPMTarget.resources + swiftPMTarget.ignored + swiftPMTarget.others).map { SourceItem( uri: DocumentURI($0), kind: $0.isDirectory ? .directory : .file, From 8fd30908e75d0434b71cf0842df3408175187cb7 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 5 Dec 2024 10:37:04 -0500 Subject: [PATCH 18/41] handle *.md and *.tutorial files for Swift DocC --- .../BuildSystemManager.swift | 2 +- .../SupportTypes/Language.swift | 2 + Sources/SKTestSupport/Utils.swift | 4 + Sources/SourceKitLSP/CMakeLists.txt | 3 + .../DocumentationLanguageService.swift | 296 ++++++++++++++++++ Sources/SourceKitLSP/LanguageServerType.swift | 5 + .../DocumentationLanguageServiceTests.swift | 41 +++ 7 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift create mode 100644 Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 801c9e22f..1ac1cd795 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -632,7 +632,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler { } switch language { - case .swift: + case .swift, .markdown, .tutorial: return await toolchainRegistry.preferredToolchain(containing: [\.sourcekitd, \.swift, \.swiftc]) case .c, .cpp, .objective_c, .objective_cpp: return await toolchainRegistry.preferredToolchain(containing: [\.clang, \.clangd]) diff --git a/Sources/LanguageServerProtocol/SupportTypes/Language.swift b/Sources/LanguageServerProtocol/SupportTypes/Language.swift index 533a6452e..b3e8076f3 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/Language.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/Language.swift @@ -92,6 +92,7 @@ extension Language: CustomStringConvertible, CustomDebugStringConvertible { case .shellScript: return "Shell Script (Bash)" case .sql: return "SQL" case .swift: return "Swift" + case .tutorial: return "Tutorial" case .typeScript: return "TypeScript" case .typeScriptReact: return "TypeScript React" case .tex: return "TeX" @@ -153,6 +154,7 @@ public extension Language { static let shellScript = Language(rawValue: "shellscript") // Shell Script (Bash) static let sql = Language(rawValue: "sql") static let swift = Language(rawValue: "swift") + static let tutorial = Language(rawValue: "tutorial") static let typeScript = Language(rawValue: "typescript") static let typeScriptReact = Language(rawValue: "typescriptreact") // TypeScript React static let tex = Language(rawValue: "tex") diff --git a/Sources/SKTestSupport/Utils.swift b/Sources/SKTestSupport/Utils.swift index 2d511901d..48dc1ff63 100644 --- a/Sources/SKTestSupport/Utils.swift +++ b/Sources/SKTestSupport/Utils.swift @@ -26,6 +26,8 @@ extension Language { var fileExtension: String { switch self { case .objective_c: return "m" + case .markdown: return "md" + case .tutorial: return "tutorial" default: return self.rawValue } } @@ -37,6 +39,8 @@ extension Language { case "m": self = .objective_c case "mm": self = .objective_cpp case "swift": self = .swift + case "md": self = .markdown + case "tutorial": self = .tutorial default: return nil } } diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index fdca6b335..8e4fc81e3 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -24,6 +24,9 @@ target_sources(SourceKitLSP PRIVATE Clang/ClangLanguageService.swift Clang/SemanticTokenTranslator.swift ) +target_sources(SourceKitLSP PRIVATE + Documentation/DocumentationLanguageService.swift +) target_sources(SourceKitLSP PRIVATE Swift/AdjustPositionToStartOfIdentifier.swift Swift/CodeActions/AddDocumentation.swift diff --git a/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift b/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift new file mode 100644 index 000000000..9fbc24ebb --- /dev/null +++ b/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift @@ -0,0 +1,296 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 BuildSystemIntegration +import Csourcekitd +import Dispatch +import Foundation +import IndexStoreDB +import SKLogging +import SemanticIndex +import SwiftExtensions +import SwiftParser +import SwiftParserDiagnostics + +#if compiler(>=6) +package import LanguageServerProtocol +package import SKOptions +package import SwiftSyntax +package import ToolchainRegistry +#else +import LanguageServerProtocol +import SKOptions +import SwiftSyntax +import ToolchainRegistry +#endif + +package actor DocumentationLanguageService: LanguageService, Sendable { + package init?( + sourceKitLSPServer: SourceKitLSPServer, + toolchain: Toolchain, + options: SourceKitLSPOptions, + testHooks: TestHooks, + workspace: Workspace + ) async throws {} + + package nonisolated func canHandle(workspace: Workspace) -> Bool { + return true + } + + package func initialize( + _ initialize: InitializeRequest + ) async throws -> InitializeResult { + return InitializeResult( + capabilities: ServerCapabilities() + ) + } + + package func clientInitialized(_ initialized: InitializedNotification) async { + // Nothing to set up + } + + package func shutdown() async { + // Nothing to tear down + } + + package func addStateChangeHandler( + handler: @escaping @Sendable (LanguageServerState, LanguageServerState) -> Void + ) async { + // There is no underlying language server with which to report state + } + + package func openDocument( + _ notification: DidOpenTextDocumentNotification, + snapshot: DocumentSnapshot + ) async { + // The DocumentationLanguageService does not do anything with document events + } + + package func closeDocument(_ notification: DidCloseTextDocumentNotification) async { + // The DocumentationLanguageService does not do anything with document events + } + + package func reopenDocument(_ notification: ReopenTextDocumentNotification) async { + // The DocumentationLanguageService does not do anything with document events + } + + package func changeDocument( + _ notification: DidChangeTextDocumentNotification, + preEditSnapshot: DocumentSnapshot, + postEditSnapshot: DocumentSnapshot, + edits: [SwiftSyntax.SourceEdit] + ) async { + // The DocumentationLanguageService does not do anything with document events + } + + package func willSaveDocument(_ notification: WillSaveTextDocumentNotification) async { + // The DocumentationLanguageService does not do anything with document events + } + + package func didSaveDocument(_ notification: DidSaveTextDocumentNotification) async { + // The DocumentationLanguageService does not do anything with document events + } + + package func documentUpdatedBuildSettings(_ uri: DocumentURI) async { + // The DocumentationLanguageService does not do anything with document events + } + + package func documentDependenciesUpdated(_ uris: Set) async { + // The DocumentationLanguageService does not do anything with document events + } + + package func completion(_ req: CompletionRequest) async throws -> CompletionList { + .init(isIncomplete: false, items: []) + } + + package func hover(_ req: HoverRequest) async throws -> HoverResponse? { + .none + } + + package func symbolInfo( + _ request: SymbolInfoRequest + ) async throws -> [SymbolDetails] { + [] + } + + package func openGeneratedInterface( + document: DocumentURI, + moduleName: String, + groupName: String?, + symbolUSR symbol: String? + ) async throws -> GeneratedInterfaceDetails? { + nil + } + + package func definition( + _ request: DefinitionRequest + ) async throws -> LocationsOrLocationLinksResponse? { + nil + } + + package func declaration( + _ request: DeclarationRequest + ) async throws -> LocationsOrLocationLinksResponse? { + nil + } + + package func documentSymbolHighlight( + _ req: DocumentHighlightRequest + ) async throws -> [DocumentHighlight]? { + nil + } + + package func foldingRange( + _ req: FoldingRangeRequest + ) async throws -> [FoldingRange]? { + nil + } + + package func documentSymbol( + _ req: DocumentSymbolRequest + ) async throws -> DocumentSymbolResponse? { + nil + } + + package func documentColor( + _ req: DocumentColorRequest + ) async throws -> [ColorInformation] { + [] + } + + package func documentSemanticTokens( + _ req: DocumentSemanticTokensRequest + ) async throws -> DocumentSemanticTokensResponse? { + nil + } + + package func documentSemanticTokensDelta( + _ req: DocumentSemanticTokensDeltaRequest + ) async throws -> DocumentSemanticTokensDeltaResponse? { + nil + } + + package func documentSemanticTokensRange( + _ req: DocumentSemanticTokensRangeRequest + ) async throws -> DocumentSemanticTokensResponse? { + nil + } + + package func colorPresentation( + _ req: ColorPresentationRequest + ) async throws -> [ColorPresentation] { + [] + } + + package func codeAction( + _ req: CodeActionRequest + ) async throws -> CodeActionRequestResponse? { + nil + } + + package func inlayHint( + _ req: InlayHintRequest + ) async throws -> [InlayHint] { + [] + } + + package func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] { + [] + } + + package func documentDiagnostic( + _ req: DocumentDiagnosticsRequest + ) async throws -> DocumentDiagnosticReport { + .full(.init(items: [])) + } + + package func documentFormatting( + _ req: DocumentFormattingRequest + ) async throws -> [TextEdit]? { + nil + } + + package func documentRangeFormatting( + _ req: LanguageServerProtocol.DocumentRangeFormattingRequest + ) async throws -> [LanguageServerProtocol.TextEdit]? { + return nil + } + + package func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? { + return nil + } + + package func rename( + _ request: RenameRequest + ) async throws -> (edits: WorkspaceEdit, usr: String?) { + (edits: .init(), usr: nil) + } + + package func editsToRename( + locations renameLocations: [RenameLocation], + in snapshot: DocumentSnapshot, + oldName: CrossLanguageName, + newName: CrossLanguageName + ) async throws -> [TextEdit] { + [] + } + + package func prepareRename( + _ request: PrepareRenameRequest + ) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? { + nil + } + + package func indexedRename( + _ request: IndexedRenameRequest + ) async throws -> WorkspaceEdit? { + nil + } + + package func editsToRenameParametersInFunctionBody( + snapshot: DocumentSnapshot, + renameLocation: RenameLocation, + newName: CrossLanguageName + ) async -> [TextEdit] { + [] + } + + package func executeCommand( + _ req: ExecuteCommandRequest + ) async throws -> LSPAny? { + nil + } + + package func getReferenceDocument( + _ req: GetReferenceDocumentRequest + ) async throws -> GetReferenceDocumentResponse { + .init(content: "") + } + + package func syntacticDocumentTests( + for uri: DocumentURI, + in workspace: Workspace + ) async throws -> [AnnotatedTestItem]? { + nil + } + + package func canonicalDeclarationPosition( + of position: Position, + in uri: DocumentURI + ) async -> Position? { + nil + } + + package func crash() async { + // There's no way to crash the DocumentationLanguageService + } +} diff --git a/Sources/SourceKitLSP/LanguageServerType.swift b/Sources/SourceKitLSP/LanguageServerType.swift index a9b99eb79..41796139f 100644 --- a/Sources/SourceKitLSP/LanguageServerType.swift +++ b/Sources/SourceKitLSP/LanguageServerType.swift @@ -17,6 +17,7 @@ import LanguageServerProtocol enum LanguageServerType: Hashable { case clangd case swift + case documentation init?(language: Language) { switch language { @@ -24,6 +25,8 @@ enum LanguageServerType: Hashable { self = .clangd case .swift: self = .swift + case .markdown, .tutorial: + self = .documentation default: return nil } @@ -44,6 +47,8 @@ enum LanguageServerType: Hashable { return ClangLanguageService.self case .swift: return SwiftLanguageService.self + case .documentation: + return DocumentationLanguageService.self } } } diff --git a/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift b/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift new file mode 100644 index 000000000..15864a797 --- /dev/null +++ b/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 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 LanguageServerProtocol +import SKTestSupport +import SourceKitLSP +import XCTest + +final class DocumentationLanguageServiceTests: XCTestCase { + func testHandlesMarkdownFiles() async throws { + try await assertHandles(language: .markdown) + } + + func testHandlesTutorialFiles() async throws { + try await assertHandles(language: .tutorial) + } +} + +fileprivate func assertHandles(language: Language) async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: language) + testClient.openDocument("", uri: uri) + + // The DocumentationLanguageService doesn't do much right now except to enable handling `*.md` + // and `*.tutorial` files for the purposes of fulfilling documentation requests. We'll just + // issue a completion request here to make sure that an empty list is returned and that + // SourceKit-LSP does not respond with an error on requests for Markdown and Tutorial files. + let completions = try await testClient.send( + CompletionRequest(textDocument: .init(uri), position: .init(line: 0, utf16index: 0)) + ) + XCTAssertEqual(completions, .init(isIncomplete: false, items: [])) +} From 94465206532e452851a5eefd46d6b69d9e98ea0f Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 5 Dec 2024 11:27:57 -0500 Subject: [PATCH 19/41] fix copyright header --- Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift b/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift index 15864a797..d9607f56d 100644 --- a/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift +++ b/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Copyright (c) 2024 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 From 60beed85ae3a88480e03dcd6a75fd192ea6d626d Mon Sep 17 00:00:00 2001 From: Josh Caswell Date: Mon, 11 Nov 2024 14:44:07 -0800 Subject: [PATCH 20/41] Customize closure expansion behavior This resolves , following the discussion of alternatives on . The bulk of the change updates the translation from SourceKit placeholders to LSP placeholders to handle nesting. The `CodeCompletionSession` also passes a new custom formatter to the swift-syntax expansion routine, which disables the transformation to trailing closures. --- Sources/SourceKitLSP/CMakeLists.txt | 1 + .../Swift/ClosureCompletionFormat.swift | 73 ++++++++ .../Swift/CodeCompletionSession.swift | 11 +- .../Swift/RewriteSourceKitPlaceholders.swift | 170 ++++++++++++++++-- .../ClosureCompletionFormatTests.swift | 130 ++++++++++++++ .../RewriteSourceKitPlaceholdersTests.swift | 66 +++++++ .../SwiftCompletionTests.swift | 88 ++++----- 7 files changed, 459 insertions(+), 80 deletions(-) create mode 100644 Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift create mode 100644 Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift create mode 100644 Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index fdca6b335..da1e045f8 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -26,6 +26,7 @@ target_sources(SourceKitLSP PRIVATE ) target_sources(SourceKitLSP PRIVATE Swift/AdjustPositionToStartOfIdentifier.swift + Swift/ClosureCompletionFormat.swift Swift/CodeActions/AddDocumentation.swift Swift/CodeActions/ConvertIntegerLiteral.swift Swift/CodeActions/ConvertJSONToCodableStruct.swift diff --git a/Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift b/Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift new file mode 100644 index 000000000..ad628aced --- /dev/null +++ b/Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.0) +public import SwiftBasicFormat +public import SwiftSyntax +#else +import SwiftBasicFormat +import SwiftSyntax +#endif + +/// A specialization of `BasicFormat` for closure literals in a code completion +/// context. +/// +/// This is more conservative about newline insertion: unless the closure has +/// multiple statements in its body it will not be reformatted to multiple +/// lines. +@_spi(Testing) +public class ClosureCompletionFormat: BasicFormat { + @_spi(Testing) + public override func requiresNewline( + between first: TokenSyntax?, + and second: TokenSyntax? + ) -> Bool { + if let first, isEndOfSmallClosureSignature(first) { + return false + } else if let first, isSmallClosureDelimiter(first, kind: \.leftBrace) { + return false + } else if let second, isSmallClosureDelimiter(second, kind: \.rightBrace) { + return false + } else { + return super.requiresNewline(between: first, and: second) + } + } + + /// Returns `true` if `token` is an opening or closing brace (according to + /// `kind`) of a closure, and that closure has no more than one statement in + /// its body. + private func isSmallClosureDelimiter( + _ token: TokenSyntax, + kind: KeyPath + ) -> Bool { + guard token.keyPathInParent == kind, + let closure = token.parent?.as(ClosureExprSyntax.self) + else { + return false + } + + return closure.statements.count <= 1 + } + + /// Returns `true` if `token` is the last token in the signature of a closure, + /// and that closure has no more than one statement in its body. + private func isEndOfSmallClosureSignature(_ token: TokenSyntax) -> Bool { + guard + token.keyPathInParent == \ClosureSignatureSyntax.inKeyword, + let closure = token.ancestorOrSelf(mapping: { $0.as(ClosureExprSyntax.self) }) + else { + return false + } + + return closure.statements.count <= 1 + } +} diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index 54b645379..a573996dd 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -323,9 +323,14 @@ class CodeCompletionSession { var parser = Parser(exprToExpand) let expr = ExprSyntax.parse(from: &parser) guard let call = OutermostFunctionCallFinder.findOutermostFunctionCall(in: expr), - let expandedCall = ExpandEditorPlaceholdersToTrailingClosures.refactor( + let expandedCall = ExpandEditorPlaceholdersToLiteralClosures.refactor( syntax: call, - in: ExpandEditorPlaceholdersToTrailingClosures.Context(indentationWidth: indentationWidth) + in: ExpandEditorPlaceholdersToLiteralClosures.Context( + format: .custom( + ClosureCompletionFormat(indentationWidth: indentationWidth), + allowNestedPlaceholders: true + ) + ) ) else { return nil @@ -334,7 +339,7 @@ class CodeCompletionSession { let bytesToExpand = Array(exprToExpand.utf8) var expandedBytes: [UInt8] = [] - // Add the prefix that we stripped of to allow expression parsing + // Add the prefix that we stripped off to allow expression parsing expandedBytes += strippedPrefix.utf8 // Add any part of the expression that didn't end up being part of the function call expandedBytes += bytesToExpand[0.. String { - var result = string - var index = 1 - while let start = result.range(of: "<#") { - guard let end = result[start.upperBound...].range(of: "#>") else { - logger.fault("Invalid placeholder in \(string)") - return string - } - let rawPlaceholder = String(result[start.lowerBound..` — in `input` to LSP +/// placeholder syntax: `${n:foo}`. +/// +/// If `clientSupportsSnippets` is `false`, the placeholder is rendered as an +/// empty string, to prevent the client from inserting special placeholder +/// characters as if they were literal text. +@_spi(Testing) +public func rewriteSourceKitPlaceholders(in input: String, clientSupportsSnippets: Bool) -> String { + var result = "" + var nextPlaceholderNumber = 1 + // Current stack of nested placeholders, most nested last. Each element needs + // to be rendered inside the element before it. + var placeholders: [(number: Int, contents: String)] = [] + let tokens = tokenize(input) + for token in tokens { + switch token { + case let .text(text): + if placeholders.isEmpty { + result += text + } else { + placeholders.latest.contents += text + } + + case let .curlyBrace(brace): + if placeholders.isEmpty { + result.append(brace) + } else { + // Braces are only escaped _inside_ a placeholder; otherwise the client + // would include the backslashes literally. + placeholders.latest.contents.append(contentsOf: ["\\", brace]) + } + + case .placeholderOpen: + placeholders.append((number: nextPlaceholderNumber, contents: "")) + nextPlaceholderNumber += 1 + + case .placeholderClose: + guard let (number, placeholderBody) = placeholders.popLast() else { + logger.fault("Invalid placeholder in \(input)") + return input + } + guard let displayName = nameForSnippet(placeholderBody) else { + logger.fault("Failed to decode placeholder \(placeholderBody) in \(input)") + return input + } + let placeholder = + clientSupportsSnippets + ? formatLSPPlaceholder(displayName, number: number) + : "" + if placeholders.isEmpty { + result += placeholder + } else { + placeholders.latest.contents += placeholder + } } - let snippet = clientSupportsSnippets ? "${\(index):\(displayName)}" : "" - result.replaceSubrange(start.lowerBound.. String? { - var text = text +/// Scan `input` to identify special elements within: curly braces, which may +/// need to be escaped; and SourceKit placeholder open/close delimiters. +private func tokenize(_ input: String) -> [SnippetToken] { + var index = input.startIndex + var isAtEnd: Bool { index == input.endIndex } + func match(_ char: Character) -> Bool { + if isAtEnd || input[index] != char { + return false + } else { + input.formIndex(after: &index) + return true + } + } + func next() -> Character? { + guard !isAtEnd else { return nil } + defer { input.formIndex(after: &index) } + return input[index] + } + + var tokens: [SnippetToken] = [] + var text = "" + while let char = next() { + switch char { + case "<": + if match("#") { + tokens.append(.text(text)) + text.removeAll() + tokens.append(.placeholderOpen) + } else { + text.append(char) + } + + case "#": + if match(">") { + tokens.append(.text(text)) + text.removeAll() + tokens.append(.placeholderClose) + } else { + text.append(char) + } + + case "{", "}": + tokens.append(.text(text)) + text.removeAll() + tokens.append(.curlyBrace(char)) + + case let c: + text.append(c) + } + } + + tokens.append(.text(text)) + + return tokens +} + +/// A syntactical element inside a SourceKit snippet. +private enum SnippetToken { + /// A placeholder delimiter. + case placeholderOpen, placeholderClose + /// One of '{' or '}', which may need to be escaped in the output. + case curlyBrace(Character) + /// Any other consecutive run of characters from the input, which needs no + /// special treatment. + case text(String) +} + +/// Given the interior text of a SourceKit placeholder, extract a display name +/// suitable for a LSP snippet. +private func nameForSnippet(_ body: String) -> String? { + var text = rewrappedAsPlaceholder(body) return text.withSyntaxText { guard let data = RawEditorPlaceholderData(syntaxText: $0) else { return nil @@ -45,3 +152,28 @@ fileprivate func nameForSnippet(_ text: String) -> String? { return String(syntaxText: data.typeForExpansionText ?? data.displayText) } } + +private let placeholderStart = "<#" +private let placeholderEnd = "#>" +private func rewrappedAsPlaceholder(_ body: String) -> String { + return placeholderStart + body + placeholderEnd +} + +/// Wrap `body` in LSP snippet placeholder syntax, using `number` as the +/// placeholder's index in the snippet. +private func formatLSPPlaceholder(_ body: String, number: Int) -> String { + "${\(number):\(body)}" +} + +private extension Array { + /// Mutable access to the final element of an array. + /// + /// - precondition: The array must not be empty. + var latest: Element { + get { self.last! } + _modify { + let index = self.index(before: self.endIndex) + yield &self[index] + } + } +} diff --git a/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift b/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift new file mode 100644 index 000000000..6ec213001 --- /dev/null +++ b/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import SourceKitLSP +import Swift +import SwiftBasicFormat +import SwiftParser +import SwiftSyntax +import SwiftSyntaxBuilder +import XCTest + +fileprivate func assertFormatted( + tree: T, + expected: String, + using format: ClosureCompletionFormat = ClosureCompletionFormat(indentationWidth: .spaces(4)), + file: StaticString = #filePath, + line: UInt = #line +) { + XCTAssertEqual(tree.formatted(using: format).description, expected, file: file, line: line) +} + +fileprivate func assertFormatted( + source: String, + expected: String, + using format: ClosureCompletionFormat = ClosureCompletionFormat(indentationWidth: .spaces(4)), + file: StaticString = #filePath, + line: UInt = #line +) { + assertFormatted( + tree: Parser.parse(source: source), + expected: expected, + using: format, + file: file, + line: line + ) +} + +final class ClosureCompletionFormatTests: XCTestCase { + func testSingleStatementClosureArg() { + assertFormatted( + source: """ + foo(bar: { baz in baz.quux }) + """, + expected: """ + foo(bar: { baz in baz.quux }) + """ + ) + } + + func testSingleStatementClosureArgAlreadyMultiLine() { + assertFormatted( + source: """ + foo( + bar: { baz in + baz.quux + } + ) + """, + expected: """ + foo( + bar: { baz in + baz.quux + } + ) + """ + ) + } + + func testMultiStatmentClosureArg() { + assertFormatted( + source: """ + foo( + bar: { baz in print(baz); return baz.quux } + ) + """, + expected: """ + foo( + bar: { baz in + print(baz); + return baz.quux + } + ) + """ + ) + } + + func testMultiStatementClosureArgAlreadyMultiLine() { + assertFormatted( + source: """ + foo( + bar: { baz in + print(baz) + return baz.quux + } + ) + """, + expected: """ + foo( + bar: { baz in + print(baz) + return baz.quux + } + ) + """ + ) + } + + func testFormatClosureWithInitialIndentation() throws { + assertFormatted( + tree: ClosureExprSyntax( + statements: CodeBlockItemListSyntax([ + CodeBlockItemSyntax(item: CodeBlockItemSyntax.Item(IntegerLiteralExprSyntax(integerLiteral: 2))) + ]) + ), + expected: """ + { 2 } + """, + using: ClosureCompletionFormat(initialIndentation: .spaces(4)) + ) + } +} diff --git a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift new file mode 100644 index 000000000..c2131cbf0 --- /dev/null +++ b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 SKTestSupport +@_spi(Testing) import SourceKitLSP +import XCTest + +final class RewriteSourceKitPlaceholdersTests: XCTestCase { + func testClientDoesNotSupportSnippets() { + let input = "foo(bar: <#T##Int##Int#>)" + let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: false) + + XCTAssertEqual(rewritten, "foo(bar: )") + } + + func testInputWithoutPlaceholders() { + let input = "foo()" + let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) + + XCTAssertEqual(rewritten, "foo()") + } + + func testPlaceholderWithType() { + let input = "foo(bar: <#T##bar##Int#>)" + let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) + + XCTAssertEqual(rewritten, "foo(bar: ${1:Int})") + } + + func testMultiplePlaceholders() { + let input = "foo(bar: <#T##Int##Int#>, baz: <#T##String##String#>, quux: <#T##String##String#>)" + let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) + + XCTAssertEqual(rewritten, "foo(bar: ${1:Int}, baz: ${2:String}, quux: ${3:String})") + } + + func testClosurePlaceholderReturnType() { + let input = "foo(bar: <#{ <#T##Int##Int#> }#>)" + let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) + + XCTAssertEqual(rewritten, #"foo(bar: ${1:\{ ${2:Int} \}})"#) + } + + func testClosurePlaceholderArgumentType() { + let input = "foo(bar: <#{ <#T##Int##Int#> in <#T##Void##Void#> }#>)" + let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) + + XCTAssertEqual(rewritten, #"foo(bar: ${1:\{ ${2:Int} in ${3:Void} \}})"#) + } + + func testMultipleClosurePlaceholders() { + let input = "foo(<#{ <#T##Int##Int#> }#>, baz: <#{ <#Int#> in <#T##Bool##Bool#> }#>)" + let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) + + XCTAssertEqual(rewritten, #"foo(${1:\{ ${2:Int} \}}, baz: ${3:\{ ${4:Int} in ${5:Bool} \}})"#) + } +} diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 4ac1f2baf..25fa3f7f0 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -863,20 +863,16 @@ final class SwiftCompletionTests: XCTestCase { deprecated: false, sortText: nil, filterText: "myMap(:)", - insertText: """ - myMap { ${1:Int} in - ${2:Bool} - } - """, + insertText: #""" + myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) + """#, insertTextFormat: .snippet, textEdit: .textEdit( TextEdit( range: Range(positions["1️⃣"]), - newText: """ - myMap { ${1:Int} in - ${2:Bool} - } - """ + newText: #""" + myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) + """# ) ) ) @@ -911,20 +907,16 @@ final class SwiftCompletionTests: XCTestCase { deprecated: false, sortText: nil, filterText: ".myMap(:)", - insertText: """ - ?.myMap { ${1:Int} in - ${2:Bool} - } - """, + insertText: #""" + ?.myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) + """#, insertTextFormat: .snippet, textEdit: .textEdit( TextEdit( range: positions["1️⃣"].. Date: Thu, 5 Dec 2024 14:13:23 -0500 Subject: [PATCH 21/41] address review comments --- .../DocumentationLanguageService.swift | 83 +++++-------------- 1 file changed, 21 insertions(+), 62 deletions(-) diff --git a/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift b/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift index 9fbc24ebb..e1aa6afdf 100644 --- a/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift +++ b/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift @@ -10,16 +10,7 @@ // //===----------------------------------------------------------------------===// -import BuildSystemIntegration -import Csourcekitd -import Dispatch import Foundation -import IndexStoreDB -import SKLogging -import SemanticIndex -import SwiftExtensions -import SwiftParser -import SwiftParserDiagnostics #if compiler(>=6) package import LanguageServerProtocol @@ -109,16 +100,14 @@ package actor DocumentationLanguageService: LanguageService, Sendable { } package func completion(_ req: CompletionRequest) async throws -> CompletionList { - .init(isIncomplete: false, items: []) + CompletionList(isIncomplete: false, items: []) } package func hover(_ req: HoverRequest) async throws -> HoverResponse? { - .none + nil } - package func symbolInfo( - _ request: SymbolInfoRequest - ) async throws -> [SymbolDetails] { + package func symbolInfo(_ request: SymbolInfoRequest) async throws -> [SymbolDetails] { [] } @@ -131,39 +120,27 @@ package actor DocumentationLanguageService: LanguageService, Sendable { nil } - package func definition( - _ request: DefinitionRequest - ) async throws -> LocationsOrLocationLinksResponse? { + package func definition(_ request: DefinitionRequest) async throws -> LocationsOrLocationLinksResponse? { nil } - package func declaration( - _ request: DeclarationRequest - ) async throws -> LocationsOrLocationLinksResponse? { + package func declaration(_ request: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? { nil } - package func documentSymbolHighlight( - _ req: DocumentHighlightRequest - ) async throws -> [DocumentHighlight]? { + package func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? { nil } - package func foldingRange( - _ req: FoldingRangeRequest - ) async throws -> [FoldingRange]? { + package func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? { nil } - package func documentSymbol( - _ req: DocumentSymbolRequest - ) async throws -> DocumentSymbolResponse? { + package func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? { nil } - package func documentColor( - _ req: DocumentColorRequest - ) async throws -> [ColorInformation] { + package func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] { [] } @@ -185,21 +162,15 @@ package actor DocumentationLanguageService: LanguageService, Sendable { nil } - package func colorPresentation( - _ req: ColorPresentationRequest - ) async throws -> [ColorPresentation] { + package func colorPresentation(_ req: ColorPresentationRequest) async throws -> [ColorPresentation] { [] } - package func codeAction( - _ req: CodeActionRequest - ) async throws -> CodeActionRequestResponse? { + package func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? { nil } - package func inlayHint( - _ req: InlayHintRequest - ) async throws -> [InlayHint] { + package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] { [] } @@ -207,15 +178,11 @@ package actor DocumentationLanguageService: LanguageService, Sendable { [] } - package func documentDiagnostic( - _ req: DocumentDiagnosticsRequest - ) async throws -> DocumentDiagnosticReport { - .full(.init(items: [])) + package func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport { + .full(RelatedFullDocumentDiagnosticReport(items: [])) } - package func documentFormatting( - _ req: DocumentFormattingRequest - ) async throws -> [TextEdit]? { + package func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? { nil } @@ -229,10 +196,8 @@ package actor DocumentationLanguageService: LanguageService, Sendable { return nil } - package func rename( - _ request: RenameRequest - ) async throws -> (edits: WorkspaceEdit, usr: String?) { - (edits: .init(), usr: nil) + package func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) { + (edits: WorkspaceEdit(), usr: nil) } package func editsToRename( @@ -250,9 +215,7 @@ package actor DocumentationLanguageService: LanguageService, Sendable { nil } - package func indexedRename( - _ request: IndexedRenameRequest - ) async throws -> WorkspaceEdit? { + package func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit? { nil } @@ -264,16 +227,12 @@ package actor DocumentationLanguageService: LanguageService, Sendable { [] } - package func executeCommand( - _ req: ExecuteCommandRequest - ) async throws -> LSPAny? { + package func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? { nil } - package func getReferenceDocument( - _ req: GetReferenceDocumentRequest - ) async throws -> GetReferenceDocumentResponse { - .init(content: "") + package func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse { + GetReferenceDocumentResponse(content: "") } package func syntacticDocumentTests( From 9f3de1b21e56d911dd3991d8983a2e5b05ab36a8 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 5 Dec 2024 14:24:31 -0500 Subject: [PATCH 22/41] mark the tutorial language as a LSP extension --- Contributor Documentation/LSP Extensions.md | 7 +++++++ Sources/LanguageServerProtocol/SupportTypes/Language.swift | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md index d7b34bb58..3130f8615 100644 --- a/Contributor Documentation/LSP Extensions.md +++ b/Contributor Documentation/LSP Extensions.md @@ -622,3 +622,10 @@ export interface GetReferenceDocumentResult { content: string; } ``` + +## Languages + +Added a new language with the identifier `tutorial` to support the `*.tutorial` files that +Swift DocC uses to define tutorials and tutorial overviews in its documentation catalogs. +It is expected that editors send document events for `tutorial` and `markdown` files if +they wish to request information about these files from SourceKit-LSP. diff --git a/Sources/LanguageServerProtocol/SupportTypes/Language.swift b/Sources/LanguageServerProtocol/SupportTypes/Language.swift index b3e8076f3..a5600c4da 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/Language.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/Language.swift @@ -154,7 +154,7 @@ public extension Language { static let shellScript = Language(rawValue: "shellscript") // Shell Script (Bash) static let sql = Language(rawValue: "sql") static let swift = Language(rawValue: "swift") - static let tutorial = Language(rawValue: "tutorial") + static let tutorial = Language(rawValue: "tutorial") // LSP Extension: Swift DocC Tutorial static let typeScript = Language(rawValue: "typescript") static let typeScriptReact = Language(rawValue: "typescriptreact") // TypeScript React static let tex = Language(rawValue: "tex") From 990ec7fc2af532f314aa7beb96c2617f988e35c1 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 5 Dec 2024 14:43:56 -0800 Subject: [PATCH 23/41] =?UTF-8?q?Don=E2=80=99t=20implicitly=20cancel=20cod?= =?UTF-8?q?e=20completion=20requests=20on=20document=20edits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As the user types, we filter the code completion results. Cancelling the completion request on every keystroke means that we will never build the initial list of completion results for this code completion session if building that list takes longer than the user's typing cadence (eg. for global completions) and we will thus not show any completions. --- Sources/SourceKitLSP/SourceKitLSPServer.swift | 32 ++++++++++++++----- Sources/SwiftExtensions/AsyncQueue.swift | 10 ------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 6cedb6c50..c7e34356c 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -150,8 +150,9 @@ package actor SourceKitLSPServer { } } - /// For all currently handled text document requests a mapping from the document to the corresponding request ID. - private var inProgressTextDocumentRequests: [DocumentURI: Set] = [:] + /// For all currently handled text document requests a mapping from the document to the corresponding request ID and + /// the method of the request (ie. the value of `TextDocumentRequest.method`). + private var inProgressTextDocumentRequests: [DocumentURI: [(id: RequestID, requestMethod: String)]] = [:] var onExit: () -> Void @@ -550,18 +551,26 @@ package actor SourceKitLSPServer { // MARK: - MessageHandler extension SourceKitLSPServer: QueueBasedMessageHandler { + private enum ImplicitTextDocumentRequestCancellationReason { + case documentChanged + case documentClosed + } + package nonisolated func didReceive(notification: some NotificationType) { let textDocumentUri: DocumentURI + let cancellationReason: ImplicitTextDocumentRequestCancellationReason switch notification { case let params as DidChangeTextDocumentNotification: textDocumentUri = params.textDocument.uri + cancellationReason = .documentChanged case let params as DidCloseTextDocumentNotification: textDocumentUri = params.textDocument.uri + cancellationReason = .documentClosed default: return } textDocumentTrackingQueue.async(priority: .high) { - await self.cancelTextDocumentRequests(for: textDocumentUri) + await self.cancelTextDocumentRequests(for: textDocumentUri, reason: cancellationReason) } } @@ -582,11 +591,18 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { /// /// - Important: Should be invoked on `textDocumentTrackingQueue` to ensure that new text document requests are /// registered before a notification that triggers cancellation might come in. - private func cancelTextDocumentRequests(for uri: DocumentURI) { + private func cancelTextDocumentRequests(for uri: DocumentURI, reason: ImplicitTextDocumentRequestCancellationReason) { guard self.options.cancelTextDocumentRequestsOnEditAndCloseOrDefault else { return } - for requestID in self.inProgressTextDocumentRequests[uri, default: []] { + for (requestID, requestMethod) in self.inProgressTextDocumentRequests[uri, default: []] { + if reason == .documentChanged && requestMethod == CompletionRequest.method { + // As the user types, we filter the code completion results. Cancelling the completion request on every + // keystroke means that we will never build the initial list of completion results for this code + // completion session if building that list takes longer than the user's typing cadence (eg. for global + // completions) and we will thus not show any completions. + continue + } logger.info("Implicitly cancelling request \(requestID)") self.messageHandlingHelper.cancelRequest(id: requestID) } @@ -633,8 +649,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { /// - Important: Should be invoked on `textDocumentTrackingQueue` to ensure that new text document requests are /// registered before a notification that triggers cancellation might come in. - private func registerInProgressTextDocumentRequest(_ request: any TextDocumentRequest, id: RequestID) { - self.inProgressTextDocumentRequests[request.textDocument.uri, default: []].insert(id) + private func registerInProgressTextDocumentRequest(_ request: T, id: RequestID) { + self.inProgressTextDocumentRequests[request.textDocument.uri, default: []].append((id: id, requestMethod: T.method)) } package func handle( @@ -644,7 +660,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { ) async { defer { if let request = params as? any TextDocumentRequest { - self.inProgressTextDocumentRequests[request.textDocument.uri, default: []].remove(id) + self.inProgressTextDocumentRequests[request.textDocument.uri, default: []].removeAll { $0.id == id } } } diff --git a/Sources/SwiftExtensions/AsyncQueue.swift b/Sources/SwiftExtensions/AsyncQueue.swift index 5bbab093c..8742e2dcc 100644 --- a/Sources/SwiftExtensions/AsyncQueue.swift +++ b/Sources/SwiftExtensions/AsyncQueue.swift @@ -82,16 +82,6 @@ package final class AsyncQueue: Sendable { package init() {} - package func cancelTasks(where filter: (TaskMetadata) -> Bool) { - pendingTasks.withLock { pendingTasks in - for task in pendingTasks { - if filter(task.metadata) { - task.task.cancel() - } - } - } - } - /// Schedule a new closure to be executed on the queue. /// /// If this is a serial queue, all previously added tasks are guaranteed to From 8ea381d7be677fd9ba5e7537a309252d82937d71 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 5 Dec 2024 16:59:07 -0800 Subject: [PATCH 24/41] Only load BSP servers that support one of the language we are interested in Instead of unconditionally loading any BSP server from one of the search locations, only load those that support, C, C++, Obj-C, Obj-C++ or Swift. --- .../ExternalBuildSystemAdapter.swift | 8 +++++++- Sources/SKTestSupport/BuildServerTestProject.swift | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift b/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift index cbb0586ef..769b9776f 100644 --- a/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift +++ b/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift @@ -223,7 +223,13 @@ actor ExternalBuildSystemAdapter { for case let buildServerConfigLocation? in buildServerConfigLocations { let jsonFiles = try? FileManager.default.contentsOfDirectory(at: buildServerConfigLocation, includingPropertiesForKeys: nil) - .filter { $0.pathExtension == "json" } + .filter { + guard let config = try? BuildServerConfig.load(from: $0) else { + return false + } + return !Set([Language.c, .cpp, .objective_c, .objective_cpp, .swift].map(\.rawValue)) + .intersection(config.languages).isEmpty + } if let configFileURL = jsonFiles?.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }).first { return configFileURL diff --git a/Sources/SKTestSupport/BuildServerTestProject.swift b/Sources/SKTestSupport/BuildServerTestProject.swift index 71033a736..1d37900da 100644 --- a/Sources/SKTestSupport/BuildServerTestProject.swift +++ b/Sources/SKTestSupport/BuildServerTestProject.swift @@ -63,10 +63,10 @@ package class BuildServerTestProject: MultiFileTestProject { var files = files files[buildServerConfigLocation] = """ { - "name": "client name", - "version": "10", + "name": "Test BSP-server", + "version": "1", "bspVersion": "2.0", - "languages": ["a", "b"], + "languages": ["swift"], "argv": ["server.py"] } """ From 5914efd9b4f5716fd33979106b9d7040e191deee Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 5 Dec 2024 18:51:26 -0800 Subject: [PATCH 25/41] Log package loading messages to the index log --- .../BuildSystemIntegration/SwiftPMBuildSystem.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index b3f16c3c0..4d0937f98 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -214,9 +214,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { private let swiftPMWorkspace: Workspace /// A `ObservabilitySystem` from `SwiftPM` that logs. - private let observabilitySystem = ObservabilitySystem({ scope, diagnostic in - logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)") - }) + private let observabilitySystem: ObservabilitySystem // MARK: Build system state (modified on package reload) @@ -280,6 +278,13 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { self.testHooks = testHooks self.connectionToSourceKitLSP = connectionToSourceKitLSP + self.observabilitySystem = ObservabilitySystem({ scope, diagnostic in + connectionToSourceKitLSP.send( + OnBuildLogMessageNotification(type: .info, task: TaskId(id: "swiftpm-log"), message: diagnostic.description) + ) + logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)") + }) + guard let destinationToolchainBinDir = toolchain.swiftc?.deletingLastPathComponent() else { throw Error.cannotDetermineHostToolchain } From 1c9a15eeb0ca800cb9401eb07234656d367efaaa Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 5 Dec 2024 18:51:41 -0800 Subject: [PATCH 26/41] Log which file caused the package to be reloaded --- Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index b3f16c3c0..7389d1530 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -764,8 +764,10 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { } package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) async { - if notification.changes.contains(where: { self.fileEventShouldTriggerPackageReload(event: $0) }) { - logger.log("Reloading package because of file change") + if let packageReloadTriggerEvent = notification.changes.first(where: { + self.fileEventShouldTriggerPackageReload(event: $0) + }) { + logger.log("Reloading package because \(packageReloadTriggerEvent.uri.forLogging) changed") await packageLoadingQueue.async { await orLog("Reloading package") { try await self.reloadPackageAssumingOnPackageLoadingQueue() From bb907a53f5df39363e512ebd786b36cd6bc2413f Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 5 Dec 2024 19:13:45 -0800 Subject: [PATCH 27/41] =?UTF-8?q?Don=E2=80=99t=20re-index=20file=20if=20we?= =?UTF-8?q?=20are=20waiting=20for=20its=20preparation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes an issue that caused the index progress to not make progress at the beginning when opening a new project because of the following: When opening the project, we would cause package resolution and schedule indexing of all source files after the package has been resolved (and dependencies been downloaded). But dependency resolution will add the dependency files to be written to disk and the editor will inform us of these modified files, which will cause us to schedule a new index operation for these files. Thus, when the initial indexing of the files finishes, we won’t increase the number of tasks that we have already indexed. To fix this, don’t schedule new indexing of these files if we are still waiting for their preparation to start. This should not be a big performance win because even before this change, when we were trying to index the file the second time, we would have realized that the index store is up to date and thus do a no-op. The main benefit of this change is to have the index counter be more accurate. --- .../SemanticIndex/SemanticIndexManager.swift | 74 +++++++++++++++---- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index 9f13d296b..347e049ec 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -51,7 +51,8 @@ private struct OpaqueQueuedIndexTask: Equatable { } private enum InProgressIndexStore { - /// We are waiting for preparation of the file's target to finish before we can index it. + /// We are waiting for preparation of the file's target to be scheduled. The next step is that we wait for + /// prepration to finish before we can update the index store for this file. /// /// `preparationTaskID` identifies the preparation task so that we can transition a file's index state to /// `updatingIndexStore` when its preparation task has finished. @@ -60,6 +61,16 @@ private enum InProgressIndexStore { /// task is still the sole owner of it and responsible for its cancellation. case waitingForPreparation(preparationTaskID: UUID, indexTask: Task) + /// We have started preparing this file and are waiting for preparation to finish before we can update the index + /// store for this file. + /// + /// `preparationTaskID` identifies the preparation task so that we can transition a file's index state to + /// `updatingIndexStore` when its preparation task has finished. + /// + /// `indexTask` is a task that finishes after both preparation and index store update are done. Whoever owns the index + /// task is still the sole owner of it and responsible for its cancellation. + case preparing(preparationTaskID: UUID, indexTask: Task) + /// The file's target has been prepared and we are updating the file's index store. /// /// `updateIndexStoreTask` is the task that updates the index store itself. @@ -210,7 +221,7 @@ package final actor SemanticIndexManager { } let indexTasks = inProgressIndexTasks.mapValues { status in switch status { - case .waitingForPreparation: + case .waitingForPreparation, .preparing: return IndexTaskStatus.scheduled case .updatingIndexStore(updateIndexStoreTask: let updateIndexStoreTask, indexTask: _): return updateIndexStoreTask.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled @@ -284,7 +295,17 @@ package final actor SemanticIndexManager { } if !indexFilesWithUpToDateUnit { let index = index.checked(for: .modifiedFiles) - filesToIndex = filesToIndex.filter { !index.hasUpToDateUnit(for: $0) } + filesToIndex = filesToIndex.filter { + if index.hasUpToDateUnit(for: $0) { + return false + } + if case .waitingForPreparation = inProgressIndexTasks[$0] { + // We haven't started preparing the file yet. Scheduling a new index operation for it won't produce any + // more recent results. + return false + } + return true + } } await scheduleBackgroundIndex(files: filesToIndex, indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit) generateBuildGraphTask = nil @@ -312,11 +333,11 @@ package final actor SemanticIndexManager { for (_, status) in inProgressIndexTasks { switch status { case .waitingForPreparation(preparationTaskID: _, indexTask: let indexTask), + .preparing(preparationTaskID: _, indexTask: let indexTask), .updatingIndexStore(updateIndexStoreTask: _, indexTask: let indexTask): taskGroup.addTask { await indexTask.value } - } } await taskGroup.waitForAll() @@ -462,7 +483,9 @@ package final actor SemanticIndexManager { private func prepare( targets: [BuildTargetIdentifier], purpose: TargetPreparationPurpose, - priority: TaskPriority? + priority: TaskPriority?, + executionStatusChangedCallback: @escaping (QueuedTask, TaskExecutionState) async -> Void = + { _, _ in } ) async { // Perform a quick initial check whether the target is up-to-date, in which case we don't need to schedule a // preparation operation at all. @@ -490,6 +513,7 @@ package final actor SemanticIndexManager { return } let preparationTask = await indexTaskScheduler.schedule(priority: priority, taskDescription) { task, newState in + await executionStatusChangedCallback(task, newState) guard case .finished = newState else { self.indexProgressStatusDidChange() return @@ -547,28 +571,36 @@ package final actor SemanticIndexManager { testHooks: testHooks ) ) + let updateIndexTask = await indexTaskScheduler.schedule(priority: priority, taskDescription) { task, newState in guard case .finished = newState else { self.indexProgressStatusDidChange() return } for fileAndTarget in filesAndTargets { - if case .updatingIndexStore(OpaqueQueuedIndexTask(task), _) = self.inProgressIndexTasks[ - fileAndTarget.file.sourceFile - ] { - self.inProgressIndexTasks[fileAndTarget.file.sourceFile] = nil + switch self.inProgressIndexTasks[fileAndTarget.file.sourceFile] { + case .updatingIndexStore(let registeredTask, _): + if registeredTask == OpaqueQueuedIndexTask(task) { + self.inProgressIndexTasks[fileAndTarget.file.sourceFile] = nil + } + case .waitingForPreparation(let registeredTask, _), .preparing(let registeredTask, _): + if registeredTask == preparationTaskID { + self.inProgressIndexTasks[fileAndTarget.file.sourceFile] = nil + } + case nil: + break } } self.indexProgressStatusDidChange() } for fileAndTarget in filesAndTargets { - if case .waitingForPreparation(preparationTaskID, let indexTask) = inProgressIndexTasks[ - fileAndTarget.file.sourceFile - ] { + switch inProgressIndexTasks[fileAndTarget.file.sourceFile] { + case .waitingForPreparation(preparationTaskID, let indexTask), .preparing(preparationTaskID, let indexTask): inProgressIndexTasks[fileAndTarget.file.sourceFile] = .updatingIndexStore( updateIndexStoreTask: OpaqueQueuedIndexTask(updateIndexTask), indexTask: indexTask ) + default: break } } return await updateIndexTask.waitToFinishPropagatingCancellation() @@ -639,9 +671,24 @@ package final actor SemanticIndexManager { // (https://github.com/swiftlang/sourcekit-lsp/issues/1262) for targetsBatch in sortedTargets.partition(intoBatchesOfSize: 1) { let preparationTaskID = UUID() + let filesToIndex = targetsBatch.flatMap({ filesByTarget[$0]! }) + let indexTask = Task(priority: priority) { // First prepare the targets. - await prepare(targets: targetsBatch, purpose: .forIndexing, priority: priority) + await prepare(targets: targetsBatch, purpose: .forIndexing, priority: priority) { task, newState in + if case .executing = newState { + for file in filesToIndex { + if case .waitingForPreparation(preparationTaskID: preparationTaskID, indexTask: let indexTask) = + self.inProgressIndexTasks[file.sourceFile] + { + self.inProgressIndexTasks[file.sourceFile] = .preparing( + preparationTaskID: preparationTaskID, + indexTask: indexTask + ) + } + } + } + } // And after preparation is done, index the files in the targets. await withTaskGroup(of: Void.self) { taskGroup in @@ -665,7 +712,6 @@ package final actor SemanticIndexManager { } indexTasks.append(indexTask) - let filesToIndex = targetsBatch.flatMap({ filesByTarget[$0]! }) // The number of index tasks that don't currently have an in-progress task associated with it. // The denominator in the index progress should get incremented by this amount. // We don't want to increment the denominator for tasks that already have an index in progress. From 751291e14f45b5e458c46462994e88d30694f704 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 6 Dec 2024 08:05:50 -0800 Subject: [PATCH 28/41] Only show call-like occurrences in call hierarchy. `extension MyTask: AnyTask {}` includes an occurrence of `MyTask.cancel` to mark it as an override of `AnyTask.cancel` but we shouldn't show the extension in the call hierarchy. --- Sources/SourceKitLSP/SourceKitLSPServer.swift | 9 ++++ .../CallHierarchyTests.swift | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index a926df0c3..c429399f4 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -2031,6 +2031,7 @@ extension SourceKitLSPServer { // callOccurrences are all the places that any of the USRs in callableUsrs is called. // We also load the `calledBy` roles to get the method that contains the reference to this call. let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .containedBy) } + .filter(\.shouldShowInCallHierarchy) // Maps functions that call a USR in `callableUSRs` to all the called occurrences of `callableUSRs` within the // function. If a function `foo` calls `bar` multiple times, `callersToCalls[foo]` will contain two call @@ -2101,6 +2102,7 @@ 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 guard occurrence.symbol.kind.isCallable else { return nil @@ -2504,6 +2506,13 @@ extension IndexSymbolKind { } } +fileprivate extension SymbolOccurrence { + /// Whether this is a call-like occurrence that should be shown in the call hierarchy. + var shouldShowInCallHierarchy: Bool { + !roles.intersection([.addressOf, .call, .read, .reference, .write]).isEmpty + } +} + /// Simple struct for pending notifications/requests, including a cancellation handler. /// For convenience the notifications/request handlers are type erased via wrapping. fileprivate struct NotificationRequestOperation { diff --git a/Tests/SourceKitLSPTests/CallHierarchyTests.swift b/Tests/SourceKitLSPTests/CallHierarchyTests.swift index 7919a0ea7..ec5ab61a9 100644 --- a/Tests/SourceKitLSPTests/CallHierarchyTests.swift +++ b/Tests/SourceKitLSPTests/CallHierarchyTests.swift @@ -895,4 +895,56 @@ final class CallHierarchyTests: XCTestCase { ] ) } + + func testOnlyConsiderCallsAsIncomingCallOccurrences() async throws { + try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls() + + // extension MyTask: AnyTask {} includes an occurrence of `MyTask.cancel` to mark it as an override of + // `AnyTask.cancel` but we shouldn't show the extension in the call hierarchy. + let project = try await IndexedSingleSwiftFileTestProject( + """ + struct MyTask { + func cancel() {} + } + + protocol AnyTask { + func cancel() + } + + extension MyTask: AnyTask {} + + func 2️⃣foo(task: MyTask)3️⃣ { + task.1️⃣cancel() + } + """ + ) + let prepare = try await project.testClient.send( + CallHierarchyPrepareRequest( + textDocument: TextDocumentIdentifier(project.fileURI), + position: project.positions["1️⃣"] + ) + ) + let initialItem = try XCTUnwrap(prepare?.only) + let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem)) + XCTAssertEqual( + calls, + [ + CallHierarchyIncomingCall( + from: CallHierarchyItem( + name: "foo(task:)", + kind: .function, + tags: nil, + uri: project.fileURI, + range: Range(project.positions["2️⃣"]), + selectionRange: Range(project.positions["2️⃣"]), + data: .dictionary([ + "usr": .string("s:4test3foo4taskyAA6MyTaskV_tF"), + "uri": .string(project.fileURI.stringValue), + ]) + ), + fromRanges: [Range(project.positions["1️⃣"])] + ) + ] + ) + } } From 67b98cd7b2f39b413ea6e40570b37de89f43b597 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 6 Dec 2024 08:24:09 -0800 Subject: [PATCH 29/41] Batch updates to the syntactic test index on fileDidChange events This is more performant. In particular adding a new task to `indexingQueue` for each file to rescan can hit the quadratic issue in `AsyncQueue` if many files were changed. --- Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift b/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift index d2866fc55..746499e3a 100644 --- a/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift +++ b/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift @@ -129,6 +129,8 @@ actor SyntacticTestIndex { } func filesDidChange(_ events: [FileEvent]) { + var removedFiles: Set = [] + var filesToRescan: [DocumentURI] = [] for fileEvent in events { switch fileEvent.type { case .created: @@ -136,13 +138,15 @@ actor SyntacticTestIndex { // `listOfTestFilesDidChange` break case .changed: - rescanFiles([fileEvent.uri]) + filesToRescan.append(fileEvent.uri) case .deleted: - removeFilesFromIndex([fileEvent.uri]) + removedFiles.insert(fileEvent.uri) default: logger.error("Ignoring unknown FileEvent type \(fileEvent.type.rawValue) in SyntacticTestIndex") } } + removeFilesFromIndex(removedFiles) + rescanFiles(filesToRescan) } /// Called when a list of files was updated. Re-scans those files From 7c474fdc5878fde919f3c7613b63e3527e025cbf Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 6 Dec 2024 08:57:21 -0800 Subject: [PATCH 30/41] Treat `$/setTrace` as a freestanding message `$/setTrace` changes a global configuration setting but it doesn't affect the result of any other request. To avoid blocking other requests on a `$/setTrace` notification the client might send during launch, we treat it as a freestanding message. Also, we don't do anything with this notification at the moment, so it doesn't matter. --- Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift b/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift index 3eae58886..4680c7e73 100644 --- a/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift +++ b/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift @@ -141,7 +141,11 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc case let notification as ReopenTextDocumentNotification: self = .documentUpdate(notification.textDocument.uri) case is SetTraceNotification: - self = .globalConfigurationChange + // `$/setTrace` changes a global configuration setting but it doesn't affect the result of any other request. To + // avoid blocking other requests on a `$/setTrace` notification the client might send during launch, we treat it + // as a freestanding message. + // Also, we don't do anything with this notification at the moment, so it doesn't matter. + self = .freestanding case is ShowMessageNotification: self = .freestanding case let notification as WillSaveTextDocumentNotification: From 697e65acdc31531b4ece97837cb2f5246466dd1a Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 5 Dec 2024 18:52:04 -0800 Subject: [PATCH 31/41] Improve logic for build graph generation status `generateBuildGraph` was named misleadingly because the primary purpose of these tasks was to schedule indexing tasks and generating the build graph was just a necessary step for this. Also update it to take into account that multiple tasks scheduling indexing tasks might be running in parallel. --- .../SemanticIndex/SemanticIndexManager.swift | 36 ++++++++++++------- .../SourceKitLSP/IndexProgressManager.swift | 4 +-- .../BackgroundIndexingTests.swift | 2 +- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index 347e049ec..797e68c2c 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -93,7 +93,7 @@ package enum IndexTaskStatus: Comparable { /// messages to the user, we only show the highest priority task. package enum IndexProgressStatus: Sendable { case preparingFileForEditorFunctionality - case generatingBuildGraph + case schedulingIndexing case indexing(preparationTasks: [BuildTargetIdentifier: IndexTaskStatus], indexTasks: [DocumentURI: IndexTaskStatus]) case upToDate @@ -101,8 +101,8 @@ package enum IndexProgressStatus: Sendable { switch (self, other) { case (_, .preparingFileForEditorFunctionality), (.preparingFileForEditorFunctionality, _): return .preparingFileForEditorFunctionality - case (_, .generatingBuildGraph), (.generatingBuildGraph, _): - return .generatingBuildGraph + case (_, .schedulingIndexing), (.schedulingIndexing, _): + return .schedulingIndexing case ( .indexing(let selfPreparationTasks, let selfIndexTasks), .indexing(let otherPreparationTasks, let otherIndexTasks) @@ -162,9 +162,9 @@ package final actor SemanticIndexManager { private let testHooks: IndexTestHooks - /// The task to generate the build graph (resolving package dependencies, generating the build description, - /// ...). `nil` if no build graph is currently being generated. - private var generateBuildGraphTask: Task? + /// The tasks to generate the build graph (resolving package dependencies, generating the build description, + /// ...) and to schedule indexing of modified tasks. + private var scheduleIndexingTasks: [UUID: Task] = [:] private let preparationUpToDateTracker = UpToDateTracker() @@ -213,8 +213,8 @@ package final actor SemanticIndexManager { if inProgressPreparationTasks.values.contains(where: { $0.purpose == .forEditorFunctionality }) { return .preparingFileForEditorFunctionality } - if generateBuildGraphTask != nil { - return .generatingBuildGraph + if !scheduleIndexingTasks.isEmpty { + return .schedulingIndexing } let preparationTasks = inProgressPreparationTasks.mapValues { inProgressTask in return inProgressTask.task.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled @@ -275,7 +275,8 @@ package final actor SemanticIndexManager { filesToIndex: [DocumentURI]?, indexFilesWithUpToDateUnit: Bool ) async { - generateBuildGraphTask = Task(priority: .low) { + let taskId = UUID() + let generateBuildGraphTask = Task(priority: .low) { await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "build-graph-generation") { await testHooks.buildGraphGenerationDidStart?() await self.buildSystemManager.waitForUpToDateBuildGraph() @@ -308,9 +309,10 @@ package final actor SemanticIndexManager { } } await scheduleBackgroundIndex(files: filesToIndex, indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit) - generateBuildGraphTask = nil + scheduleIndexingTasks[taskId] = nil } } + scheduleIndexingTasks[taskId] = generateBuildGraphTask indexProgressStatusDidChange() } @@ -322,12 +324,22 @@ package final actor SemanticIndexManager { await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(filesToIndex: nil, indexFilesWithUpToDateUnit: true) } + private func waitForBuildGraphGenerationTasks() async { + await withTaskGroup(of: Void.self) { taskGroup in + for generateBuildGraphTask in scheduleIndexingTasks.values { + taskGroup.addTask { + await generateBuildGraphTask.value + } + } + } + } + /// Wait for all in-progress index tasks to finish. package func waitForUpToDateIndex() async { logger.info("Waiting for up-to-date index") // Wait for a build graph update first, if one is in progress. This will add all index tasks to `indexStatus`, so we // can await the index tasks below. - await generateBuildGraphTask?.value + await waitForBuildGraphGenerationTasks() await withTaskGroup(of: Void.self) { taskGroup in for (_, status) in inProgressIndexTasks { @@ -356,7 +368,7 @@ package final actor SemanticIndexManager { ) // If there's a build graph update in progress wait for that to finish so we can discover new files in the build // system. - await generateBuildGraphTask?.value + await waitForBuildGraphGenerationTasks() // Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will // - Wait for the existing index operations to finish if they have the same number of files. diff --git a/Sources/SourceKitLSP/IndexProgressManager.swift b/Sources/SourceKitLSP/IndexProgressManager.swift index d283719a9..19356f45d 100644 --- a/Sources/SourceKitLSP/IndexProgressManager.swift +++ b/Sources/SourceKitLSP/IndexProgressManager.swift @@ -93,8 +93,8 @@ actor IndexProgressManager { case .preparingFileForEditorFunctionality: message = "Preparing current file" percentage = 0 - case .generatingBuildGraph: - message = "Generating build graph" + case .schedulingIndexing: + message = "Scheduling tasks" percentage = 0 case .indexing(preparationTasks: let preparationTasks, indexTasks: let indexTasks): // We can get into a situation where queuedIndexTasks < indexTasks.count if we haven't processed all diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 8478d26cd..e3920b8f1 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -378,7 +378,7 @@ final class BackgroundIndexingTests: XCTestCase { XCTFail("Expected begin notification") return } - XCTAssertEqual(beginData.message, "Generating build graph") + XCTAssertEqual(beginData.message, "Scheduling tasks") let indexingWorkDoneProgressToken = beginNotification.token _ = try await project.testClient.nextNotification( From 388789472c454eea3b37ab6c97cf4d43c284a509 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 6 Dec 2024 11:26:13 -0800 Subject: [PATCH 32/41] Create child scopes for SwiftPM operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some SwiftPM functions check whether their observability scope has errors. If we use the same observability scope for all SwiftPM operations during SourceKit-LSP’s lifetime, a single SwiftPM error will set the `hasError` bit in that observability scope for the entirety of SourceKit-LSP’s lifetime, impacting all upcoming SwiftPM operations. Creating a separate child scope for every operation fixes --- Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index ee67dbb43..f80a5e6de 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -302,10 +302,10 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { explicitDirectory: options.swiftPMOrDefault.swiftSDKsDirectory.map { try AbsolutePath(validating: $0) } ), fileSystem: localFileSystem, - observabilityScope: observabilitySystem.topScope, + observabilityScope: observabilitySystem.topScope.makeChildScope(description: "SwiftPM Bundle Store"), outputHandler: { _ in } ), - observabilityScope: observabilitySystem.topScope, + observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Derive Target Swift SDK"), fileSystem: localFileSystem ) @@ -414,7 +414,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { let modulesGraph = try await self.swiftPMWorkspace.loadPackageGraph( rootInput: PackageGraphRootInput(packages: [AbsolutePath(validating: projectRoot.filePath)]), forceResolvedVersions: !isForIndexBuild, - observabilityScope: observabilitySystem.topScope + observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Load package graph") ) let plan = try await BuildPlan( @@ -423,7 +423,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { graph: modulesGraph, disableSandbox: options.swiftPMOrDefault.disableSandbox ?? false, fileSystem: localFileSystem, - observabilityScope: observabilitySystem.topScope + observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Create SwiftPM build plan") ) let buildDescription = BuildDescription(buildPlan: plan) self.buildDescription = buildDescription From ddbd6543c4bae982fc01318c33a0bd40fc921228 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 6 Dec 2024 11:29:52 -0800 Subject: [PATCH 33/41] Allow dependency updates in the `index-build` folder When we have background indexing enabled, SourceKit-LSP manages the dependencies. We should thus allow it to update them, eg. after `Package.resolved` was updated. --- Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index ee67dbb43..09e75a49b 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -329,7 +329,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { } var configuration = WorkspaceConfiguration.default - configuration.skipDependenciesUpdates = true + configuration.skipDependenciesUpdates = !options.backgroundIndexingOrDefault self.swiftPMWorkspace = try Workspace( fileSystem: localFileSystem, From a61bbffdb4e16cb2454dbd6825e57602965d6eac Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 5 Dec 2024 20:23:33 -0800 Subject: [PATCH 34/41] Cache path components for directories returned from build system We frequently compute if a file is descendent of the directory and `URL.pathComponents` is an expensive computation. --- .../BuildSystemManager.swift | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 1ac1cd795..0ad777677 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -338,7 +338,10 @@ package actor BuildSystemManager: QueueBasedMessageHandler { let files: [DocumentURI: SourceFileInfo] /// The source directories in the workspace, ie. all `SourceItem`s that have `kind == .directory`. - let directories: [DocumentURI: SourceFileInfo] + /// + /// `pathComponents` is the result of `key.fileURL?.pathComponents`. We frequently need these path components to + /// determine if a file is descendent of the directory and computing them from the `DocumentURI` is expensive. + let directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)] } private let cachedSourceFilesAndDirectories = Cache() @@ -679,12 +682,12 @@ package actor BuildSystemManager: QueueBasedMessageHandler { if let targets = filesAndDirectories.files[document]?.targets { result.formUnion(targets) } - if !filesAndDirectories.directories.isEmpty, let documentPath = document.fileURL { - for (directory, info) in filesAndDirectories.directories { - guard let directoryPath = directory.fileURL else { + if !filesAndDirectories.directories.isEmpty, let documentPathComponents = document.fileURL?.pathComponents { + for (directory, (directoryPathComponents, info)) in filesAndDirectories.directories { + guard let directoryPathComponents, let directoryPath = directory.fileURL else { continue } - if documentPath.isDescendant(of: directoryPath) { + if isDescendant(documentPathComponents, of: directoryPathComponents) { result.formUnion(info.targets) } } @@ -1055,7 +1058,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler { return try await cachedSourceFilesAndDirectories.get(key, isolation: self) { key in var files: [DocumentURI: SourceFileInfo] = [:] - var directories: [DocumentURI: SourceFileInfo] = [:] + var directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)] = [:] for sourcesItem in key.sourcesItems { let target = targets[sourcesItem.target]?.target let isPartOfRootProject = !(target?.tags.contains(.dependency) ?? false) @@ -1077,7 +1080,9 @@ package actor BuildSystemManager: QueueBasedMessageHandler { case .file: files[sourceItem.uri] = info.merging(files[sourceItem.uri]) case .directory: - directories[sourceItem.uri] = info.merging(directories[sourceItem.uri]) + directories[sourceItem.uri] = ( + sourceItem.uri.fileURL?.pathComponents, info.merging(directories[sourceItem.uri]?.info) + ) } } } @@ -1226,3 +1231,12 @@ package actor BuildSystemManager: QueueBasedMessageHandler { } } } + +/// Returns `true` if the path components `selfPathComponents`, retrieved from `URL.pathComponents` are a descendent +/// of the other path components. +/// +/// This operates directly on path components instead of `URL`s because computing the path components of a URL is +/// expensive and this allows us to cache the path components. +private func isDescendant(_ selfPathComponents: [String], of otherPathComponents: [String]) -> Bool { + return selfPathComponents.dropLast().starts(with: otherPathComponents) +} From 1c1a1cf5f63eb09becbbf9cded6fc7292636f144 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 5 Dec 2024 20:24:59 -0800 Subject: [PATCH 35/41] Cached transformed results in `Cache` The transform to get the transformed result might be expensive, so we should cache its result. --- .../BuildSystemManager.swift | 14 ++++++----- Sources/SwiftExtensions/Cache.swift | 25 ++++++++++++++----- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 1ac1cd795..c8d3deb37 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -1009,19 +1009,21 @@ package actor BuildSystemManager: QueueBasedMessageHandler { return [] } + let request = BuildTargetSourcesRequest(targets: targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) + // If we have a cached request for a superset of the targets, serve the result from that cache entry. let fromSuperset = await orLog("Getting source files from superset request") { - try await cachedTargetSources.get(isolation: self) { request in - targets.isSubset(of: request.targets) - } transform: { response in - return BuildTargetSourcesResponse(items: response.items.filter { targets.contains($0.target) }) - } + try await cachedTargetSources.getDerived( + isolation: self, + request, + canReuseKey: { targets.isSubset(of: $0.targets) }, + transform: { BuildTargetSourcesResponse(items: $0.items.filter { targets.contains($0.target) }) } + ) } if let fromSuperset { return fromSuperset.items } - let request = BuildTargetSourcesRequest(targets: targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) let response = try await cachedTargetSources.get(request, isolation: self) { request in try await buildSystemAdapter.send(request) } diff --git a/Sources/SwiftExtensions/Cache.swift b/Sources/SwiftExtensions/Cache.swift index 7642b20de..debb7a026 100644 --- a/Sources/SwiftExtensions/Cache.swift +++ b/Sources/SwiftExtensions/Cache.swift @@ -33,15 +33,28 @@ package class Cache { return try await task.value } - package func get( + /// Get the value cached for `key`. If no value exists for `key`, try deriving the result from an existing cache entry + /// that satisfies `canReuseKey` by applying `transform` to that result. + package func getDerived( isolation: isolated any Actor, - whereKey keyPredicate: (Key) -> Bool, - transform: @Sendable @escaping (Result) -> Result + _ key: Key, + canReuseKey: @Sendable @escaping (Key) -> Bool, + transform: @Sendable @escaping (_ cachedResult: Result) -> Result ) async throws -> Result? { - for (key, value) in storage { - if keyPredicate(key) { - return try await transform(value.value) + if let cached = storage[key] { + // If we have a value for the requested key, prefer that + return try await cached.value + } + + // See if don't have an entry for this key, see if we can derive the value from a cached entry. + for (cachedKey, cachedValue) in storage { + guard canReuseKey(cachedKey) else { + continue } + let transformed = Task { try await transform(cachedValue.value) } + // Cache the transformed result. + storage[key] = transformed + return try await transformed.value } return nil } From cbd897de03916a97fedeaa6a0d2b321c1497e8be Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 6 Dec 2024 09:01:02 -0800 Subject: [PATCH 36/41] =?UTF-8?q?Don=E2=80=99t=20show=20warning=20message?= =?UTF-8?q?=20when=20opening=20projects=20that=20don't=20support=20backgro?= =?UTF-8?q?und=20indexing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we enabled background indexing by default, the user is no longer explicitly opting into it. A user might be exclusively working with compilation database projects or BSP server without background indexing support and thus not care that we switched the background indexing default. We shouldn’t bother them with a warning message every time they launch sourcekit-lsp. --- Sources/SourceKitLSP/SourceKitLSPServer.swift | 20 ------------------- .../BackgroundIndexingTests.swift | 11 ---------- 2 files changed, 31 deletions(-) diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index a926df0c3..62c4dbae1 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -84,12 +84,6 @@ package actor SourceKitLSPServer { /// Initialization can be awaited using `waitUntilInitialized`. private var initialized: Bool = false - /// Set to `true` after the user has opened a project that doesn't support background indexing while having background - /// indexing enabled. - /// - /// This ensures that we only inform the user about background indexing not being supported for these projects once. - private var didSendBackgroundIndexingNotSupportedNotification = false - var options: SourceKitLSPOptions let testHooks: TestHooks @@ -841,20 +835,6 @@ extension SourceKitLSPServer { testHooks: testHooks, indexTaskScheduler: indexTaskScheduler ) - if options.backgroundIndexingOrDefault, workspace.semanticIndexManager == nil, - !self.didSendBackgroundIndexingNotSupportedNotification - { - self.sendNotificationToClient( - ShowMessageNotification( - type: .info, - message: """ - Background indexing is currently only supported for SwiftPM projects. \ - For all other project types, please run a build to update the index. - """ - ) - ) - self.didSendBackgroundIndexingNotSupportedNotification = true - } return workspace } diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 8478d26cd..04ac51ef5 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -885,17 +885,6 @@ final class BackgroundIndexingTests: XCTestCase { ) } - func testShowMessageWhenOpeningAProjectThatDoesntSupportBackgroundIndexing() async throws { - let project = try await MultiFileTestProject( - files: [ - "compile_commands.json": "" - ], - enableBackgroundIndexing: true - ) - let message = try await project.testClient.nextNotification(ofType: ShowMessageNotification.self) - XCTAssert(message.message.contains("Background indexing"), "Received unexpected message: \(message.message)") - } - func testNoPreparationStatusIfTargetIsUpToDate() async throws { let project = try await SwiftPMTestProject( files: [ From ee1f4b13fcca7d35d5c40f6a22004c18e71245f2 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 6 Dec 2024 12:17:31 -0800 Subject: [PATCH 37/41] =?UTF-8?q?Don=E2=80=99t=20escape=20`{`=20inside=20p?= =?UTF-8?q?laceholder=20snippets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `\{` is included inside an LSP placeholder, in VS Code will insert `\{` verbatim. At least in VS Code, we only need to escape the closing brace. While at it, also escape `$` and `\` inside placeholders, according to the LSP spec. --- .../Swift/RewriteSourceKitPlaceholders.swift | 18 ++++++++--------- .../RewriteSourceKitPlaceholdersTests.swift | 6 +++--- .../SwiftCompletionTests.swift | 20 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift b/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift index 4a9f489df..4c7444dfc 100644 --- a/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift +++ b/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift @@ -37,13 +37,13 @@ public func rewriteSourceKitPlaceholders(in input: String, clientSupportsSnippet placeholders.latest.contents += text } - case let .curlyBrace(brace): + case .escapeInsidePlaceholder(let character): if placeholders.isEmpty { - result.append(brace) + result.append(character) } else { - // Braces are only escaped _inside_ a placeholder; otherwise the client - // would include the backslashes literally. - placeholders.latest.contents.append(contentsOf: ["\\", brace]) + // A closing brace is only escaped _inside_ a placeholder; otherwise the client would include the backslashes + // literally. + placeholders.latest.contents += [#"\"#, character] } case .placeholderOpen: @@ -115,10 +115,10 @@ private func tokenize(_ input: String) -> [SnippetToken] { text.append(char) } - case "{", "}": + case "$", "}", "\\": tokens.append(.text(text)) text.removeAll() - tokens.append(.curlyBrace(char)) + tokens.append(.escapeInsidePlaceholder(char)) case let c: text.append(c) @@ -134,8 +134,8 @@ private func tokenize(_ input: String) -> [SnippetToken] { private enum SnippetToken { /// A placeholder delimiter. case placeholderOpen, placeholderClose - /// One of '{' or '}', which may need to be escaped in the output. - case curlyBrace(Character) + /// '$', '}' or '\', which need to be escaped when used inside a placeholder. + case escapeInsidePlaceholder(Character) /// Any other consecutive run of characters from the input, which needs no /// special treatment. case text(String) diff --git a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift index c2131cbf0..f7a3f8439 100644 --- a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift +++ b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift @@ -47,20 +47,20 @@ final class RewriteSourceKitPlaceholdersTests: XCTestCase { let input = "foo(bar: <#{ <#T##Int##Int#> }#>)" let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - XCTAssertEqual(rewritten, #"foo(bar: ${1:\{ ${2:Int} \}})"#) + XCTAssertEqual(rewritten, #"foo(bar: ${1:{ ${2:Int} \}})"#) } func testClosurePlaceholderArgumentType() { let input = "foo(bar: <#{ <#T##Int##Int#> in <#T##Void##Void#> }#>)" let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - XCTAssertEqual(rewritten, #"foo(bar: ${1:\{ ${2:Int} in ${3:Void} \}})"#) + XCTAssertEqual(rewritten, #"foo(bar: ${1:{ ${2:Int} in ${3:Void} \}})"#) } func testMultipleClosurePlaceholders() { let input = "foo(<#{ <#T##Int##Int#> }#>, baz: <#{ <#Int#> in <#T##Bool##Bool#> }#>)" let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - XCTAssertEqual(rewritten, #"foo(${1:\{ ${2:Int} \}}, baz: ${3:\{ ${4:Int} in ${5:Bool} \}})"#) + XCTAssertEqual(rewritten, #"foo(${1:{ ${2:Int} \}}, baz: ${3:{ ${4:Int} in ${5:Bool} \}})"#) } } diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 25fa3f7f0..19b95a72d 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -864,14 +864,14 @@ final class SwiftCompletionTests: XCTestCase { sortText: nil, filterText: "myMap(:)", insertText: #""" - myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) + myMap(${1:{ ${2:Int} in ${3:Bool} \}}) """#, insertTextFormat: .snippet, textEdit: .textEdit( TextEdit( range: Range(positions["1️⃣"]), newText: #""" - myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) + myMap(${1:{ ${2:Int} in ${3:Bool} \}}) """# ) ) @@ -908,14 +908,14 @@ final class SwiftCompletionTests: XCTestCase { sortText: nil, filterText: ".myMap(:)", insertText: #""" - ?.myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) + ?.myMap(${1:{ ${2:Int} in ${3:Bool} \}}) """#, insertTextFormat: .snippet, textEdit: .textEdit( TextEdit( range: positions["1️⃣"].. Date: Fri, 22 Nov 2024 22:31:11 +0100 Subject: [PATCH 38/41] Return compiler arguments for invalid package manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, when there‘s a syntax error in a package manifest, we don’t get any build settings from it in SourceKit-LSP and thus loose almost all semantic functionality. If we can’t parse the package manifest, fall back to providing build settings by assuming it has the current Swift tools version. Currently, when there‘s a syntax error in a package manifest, we don’t get any build settings from it in SourceKit-LSP and thus loose almost all semantic functionality. If we can’t parse the package manifest, fall back to providing build settings by assuming it has the current Swift tools version. Fixes #1704 rdar://136423767 --- .../SwiftPMBuildSystem.swift | 2 +- .../SwiftPMBuildSystemTests.swift | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 90b7cf4ca..adb790a74 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -783,7 +783,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { /// Retrieve settings for a package manifest (Package.swift). private func settings(forPackageManifest path: AbsolutePath) throws -> TextDocumentSourceKitOptionsResponse? { - let compilerArgs = swiftPMWorkspace.interpreterFlags(for: path.parentDirectory) + [path.pathString] + let compilerArgs = try swiftPMWorkspace.interpreterFlags(for: path) + [path.pathString] return TextDocumentSourceKitOptionsResponse(compilerArguments: compilerArgs) } } diff --git a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift index 339ea64f0..f800b0a2f 100644 --- a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift @@ -1148,6 +1148,39 @@ final class SwiftPMBuildSystemTests: XCTestCase { XCTAssert(compilerArgs.contains(try versionSpecificManifestURL.filePath)) } } + + func testBuildSettingsForInvalidManifest() async throws { + try await withTestScratchDir { tempDir in + try FileManager.default.createFiles( + root: tempDir, + files: [ + "pkg/Sources/lib/a.swift": "", + "pkg/Package.swift": """ + // swift-tools-version: 4.2 + import PackageDescription + """, + ] + ) + let packageRoot = try tempDir.appendingPathComponent("pkg").realpath + let manifestURL = packageRoot.appendingPathComponent("Package.swift") + let buildSystemManager = await BuildSystemManager( + buildSystemSpec: BuildSystemSpec(kind: .swiftPM, projectRoot: packageRoot), + toolchainRegistry: .forTesting, + options: SourceKitLSPOptions(), + connectionToClient: DummyBuildSystemManagerConnectionToClient(), + buildSystemTestHooks: BuildSystemTestHooks() + ) + await buildSystemManager.waitForUpToDateBuildGraph() + let settings = await buildSystemManager.buildSettingsInferredFromMainFile( + for: DocumentURI(manifestURL), + language: .swift, + fallbackAfterTimeout: false + ) + let compilerArgs = try XCTUnwrap(settings?.compilerArguments) + XCTAssert(compilerArgs.contains("-package-description-version")) + XCTAssert(compilerArgs.contains(try manifestURL.filePath)) + } + } } private func assertArgumentsDoNotContain( From 85828ee0a8e2e214e43f7ec0aec70255e35287e4 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 10 Dec 2024 10:33:14 -0800 Subject: [PATCH 39/41] Revert "Merge pull request #1870 from ahoppen/no-escape-open-brace" This reverts commit 1a708ec596a97d61b18b0919f45bd36068bac1f5, reversing changes made to 5183889e7ea5ce02ad3967e9f16b3aa414bb4a65. --- .../Swift/RewriteSourceKitPlaceholders.swift | 18 ++++++++--------- .../RewriteSourceKitPlaceholdersTests.swift | 6 +++--- .../SwiftCompletionTests.swift | 20 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift b/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift index 4c7444dfc..4a9f489df 100644 --- a/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift +++ b/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift @@ -37,13 +37,13 @@ public func rewriteSourceKitPlaceholders(in input: String, clientSupportsSnippet placeholders.latest.contents += text } - case .escapeInsidePlaceholder(let character): + case let .curlyBrace(brace): if placeholders.isEmpty { - result.append(character) + result.append(brace) } else { - // A closing brace is only escaped _inside_ a placeholder; otherwise the client would include the backslashes - // literally. - placeholders.latest.contents += [#"\"#, character] + // Braces are only escaped _inside_ a placeholder; otherwise the client + // would include the backslashes literally. + placeholders.latest.contents.append(contentsOf: ["\\", brace]) } case .placeholderOpen: @@ -115,10 +115,10 @@ private func tokenize(_ input: String) -> [SnippetToken] { text.append(char) } - case "$", "}", "\\": + case "{", "}": tokens.append(.text(text)) text.removeAll() - tokens.append(.escapeInsidePlaceholder(char)) + tokens.append(.curlyBrace(char)) case let c: text.append(c) @@ -134,8 +134,8 @@ private func tokenize(_ input: String) -> [SnippetToken] { private enum SnippetToken { /// A placeholder delimiter. case placeholderOpen, placeholderClose - /// '$', '}' or '\', which need to be escaped when used inside a placeholder. - case escapeInsidePlaceholder(Character) + /// One of '{' or '}', which may need to be escaped in the output. + case curlyBrace(Character) /// Any other consecutive run of characters from the input, which needs no /// special treatment. case text(String) diff --git a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift index f7a3f8439..c2131cbf0 100644 --- a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift +++ b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift @@ -47,20 +47,20 @@ final class RewriteSourceKitPlaceholdersTests: XCTestCase { let input = "foo(bar: <#{ <#T##Int##Int#> }#>)" let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - XCTAssertEqual(rewritten, #"foo(bar: ${1:{ ${2:Int} \}})"#) + XCTAssertEqual(rewritten, #"foo(bar: ${1:\{ ${2:Int} \}})"#) } func testClosurePlaceholderArgumentType() { let input = "foo(bar: <#{ <#T##Int##Int#> in <#T##Void##Void#> }#>)" let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - XCTAssertEqual(rewritten, #"foo(bar: ${1:{ ${2:Int} in ${3:Void} \}})"#) + XCTAssertEqual(rewritten, #"foo(bar: ${1:\{ ${2:Int} in ${3:Void} \}})"#) } func testMultipleClosurePlaceholders() { let input = "foo(<#{ <#T##Int##Int#> }#>, baz: <#{ <#Int#> in <#T##Bool##Bool#> }#>)" let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - XCTAssertEqual(rewritten, #"foo(${1:{ ${2:Int} \}}, baz: ${3:{ ${4:Int} in ${5:Bool} \}})"#) + XCTAssertEqual(rewritten, #"foo(${1:\{ ${2:Int} \}}, baz: ${3:\{ ${4:Int} in ${5:Bool} \}})"#) } } diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 19b95a72d..25fa3f7f0 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -864,14 +864,14 @@ final class SwiftCompletionTests: XCTestCase { sortText: nil, filterText: "myMap(:)", insertText: #""" - myMap(${1:{ ${2:Int} in ${3:Bool} \}}) + myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) """#, insertTextFormat: .snippet, textEdit: .textEdit( TextEdit( range: Range(positions["1️⃣"]), newText: #""" - myMap(${1:{ ${2:Int} in ${3:Bool} \}}) + myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) """# ) ) @@ -908,14 +908,14 @@ final class SwiftCompletionTests: XCTestCase { sortText: nil, filterText: ".myMap(:)", insertText: #""" - ?.myMap(${1:{ ${2:Int} in ${3:Bool} \}}) + ?.myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) """#, insertTextFormat: .snippet, textEdit: .textEdit( TextEdit( range: positions["1️⃣"].. Date: Tue, 10 Dec 2024 10:33:28 -0800 Subject: [PATCH 40/41] Revert "Merge pull request #1854 from matthewbastien/documentation-language-service" This reverts commit 9bbb8f3287f55ddae5013cf9e8a3c387886775dc, reversing changes made to 21dfaf0f9cff545e33c4d2910bf7698a41febe27. --- Contributor Documentation/LSP Extensions.md | 7 - .../BuildSystemManager.swift | 2 +- .../SupportTypes/Language.swift | 2 - Sources/SKTestSupport/Utils.swift | 4 - Sources/SourceKitLSP/CMakeLists.txt | 3 - .../DocumentationLanguageService.swift | 255 ------------------ Sources/SourceKitLSP/LanguageServerType.swift | 5 - .../DocumentationLanguageServiceTests.swift | 41 --- 8 files changed, 1 insertion(+), 318 deletions(-) delete mode 100644 Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift delete mode 100644 Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md index 3130f8615..d7b34bb58 100644 --- a/Contributor Documentation/LSP Extensions.md +++ b/Contributor Documentation/LSP Extensions.md @@ -622,10 +622,3 @@ export interface GetReferenceDocumentResult { content: string; } ``` - -## Languages - -Added a new language with the identifier `tutorial` to support the `*.tutorial` files that -Swift DocC uses to define tutorials and tutorial overviews in its documentation catalogs. -It is expected that editors send document events for `tutorial` and `markdown` files if -they wish to request information about these files from SourceKit-LSP. diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 64c818519..d1a1fd3fa 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -635,7 +635,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler { } switch language { - case .swift, .markdown, .tutorial: + case .swift: return await toolchainRegistry.preferredToolchain(containing: [\.sourcekitd, \.swift, \.swiftc]) case .c, .cpp, .objective_c, .objective_cpp: return await toolchainRegistry.preferredToolchain(containing: [\.clang, \.clangd]) diff --git a/Sources/LanguageServerProtocol/SupportTypes/Language.swift b/Sources/LanguageServerProtocol/SupportTypes/Language.swift index a5600c4da..533a6452e 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/Language.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/Language.swift @@ -92,7 +92,6 @@ extension Language: CustomStringConvertible, CustomDebugStringConvertible { case .shellScript: return "Shell Script (Bash)" case .sql: return "SQL" case .swift: return "Swift" - case .tutorial: return "Tutorial" case .typeScript: return "TypeScript" case .typeScriptReact: return "TypeScript React" case .tex: return "TeX" @@ -154,7 +153,6 @@ public extension Language { static let shellScript = Language(rawValue: "shellscript") // Shell Script (Bash) static let sql = Language(rawValue: "sql") static let swift = Language(rawValue: "swift") - static let tutorial = Language(rawValue: "tutorial") // LSP Extension: Swift DocC Tutorial static let typeScript = Language(rawValue: "typescript") static let typeScriptReact = Language(rawValue: "typescriptreact") // TypeScript React static let tex = Language(rawValue: "tex") diff --git a/Sources/SKTestSupport/Utils.swift b/Sources/SKTestSupport/Utils.swift index 48dc1ff63..2d511901d 100644 --- a/Sources/SKTestSupport/Utils.swift +++ b/Sources/SKTestSupport/Utils.swift @@ -26,8 +26,6 @@ extension Language { var fileExtension: String { switch self { case .objective_c: return "m" - case .markdown: return "md" - case .tutorial: return "tutorial" default: return self.rawValue } } @@ -39,8 +37,6 @@ extension Language { case "m": self = .objective_c case "mm": self = .objective_cpp case "swift": self = .swift - case "md": self = .markdown - case "tutorial": self = .tutorial default: return nil } } diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index 51702cce5..da1e045f8 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -24,9 +24,6 @@ target_sources(SourceKitLSP PRIVATE Clang/ClangLanguageService.swift Clang/SemanticTokenTranslator.swift ) -target_sources(SourceKitLSP PRIVATE - Documentation/DocumentationLanguageService.swift -) target_sources(SourceKitLSP PRIVATE Swift/AdjustPositionToStartOfIdentifier.swift Swift/ClosureCompletionFormat.swift diff --git a/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift b/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift deleted file mode 100644 index e1aa6afdf..000000000 --- a/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift +++ /dev/null @@ -1,255 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 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 Foundation - -#if compiler(>=6) -package import LanguageServerProtocol -package import SKOptions -package import SwiftSyntax -package import ToolchainRegistry -#else -import LanguageServerProtocol -import SKOptions -import SwiftSyntax -import ToolchainRegistry -#endif - -package actor DocumentationLanguageService: LanguageService, Sendable { - package init?( - sourceKitLSPServer: SourceKitLSPServer, - toolchain: Toolchain, - options: SourceKitLSPOptions, - testHooks: TestHooks, - workspace: Workspace - ) async throws {} - - package nonisolated func canHandle(workspace: Workspace) -> Bool { - return true - } - - package func initialize( - _ initialize: InitializeRequest - ) async throws -> InitializeResult { - return InitializeResult( - capabilities: ServerCapabilities() - ) - } - - package func clientInitialized(_ initialized: InitializedNotification) async { - // Nothing to set up - } - - package func shutdown() async { - // Nothing to tear down - } - - package func addStateChangeHandler( - handler: @escaping @Sendable (LanguageServerState, LanguageServerState) -> Void - ) async { - // There is no underlying language server with which to report state - } - - package func openDocument( - _ notification: DidOpenTextDocumentNotification, - snapshot: DocumentSnapshot - ) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func closeDocument(_ notification: DidCloseTextDocumentNotification) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func reopenDocument(_ notification: ReopenTextDocumentNotification) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func changeDocument( - _ notification: DidChangeTextDocumentNotification, - preEditSnapshot: DocumentSnapshot, - postEditSnapshot: DocumentSnapshot, - edits: [SwiftSyntax.SourceEdit] - ) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func willSaveDocument(_ notification: WillSaveTextDocumentNotification) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func didSaveDocument(_ notification: DidSaveTextDocumentNotification) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func documentUpdatedBuildSettings(_ uri: DocumentURI) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func documentDependenciesUpdated(_ uris: Set) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func completion(_ req: CompletionRequest) async throws -> CompletionList { - CompletionList(isIncomplete: false, items: []) - } - - package func hover(_ req: HoverRequest) async throws -> HoverResponse? { - nil - } - - package func symbolInfo(_ request: SymbolInfoRequest) async throws -> [SymbolDetails] { - [] - } - - package func openGeneratedInterface( - document: DocumentURI, - moduleName: String, - groupName: String?, - symbolUSR symbol: String? - ) async throws -> GeneratedInterfaceDetails? { - nil - } - - package func definition(_ request: DefinitionRequest) async throws -> LocationsOrLocationLinksResponse? { - nil - } - - package func declaration(_ request: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? { - nil - } - - package func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? { - nil - } - - package func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? { - nil - } - - package func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? { - nil - } - - package func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] { - [] - } - - package func documentSemanticTokens( - _ req: DocumentSemanticTokensRequest - ) async throws -> DocumentSemanticTokensResponse? { - nil - } - - package func documentSemanticTokensDelta( - _ req: DocumentSemanticTokensDeltaRequest - ) async throws -> DocumentSemanticTokensDeltaResponse? { - nil - } - - package func documentSemanticTokensRange( - _ req: DocumentSemanticTokensRangeRequest - ) async throws -> DocumentSemanticTokensResponse? { - nil - } - - package func colorPresentation(_ req: ColorPresentationRequest) async throws -> [ColorPresentation] { - [] - } - - package func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? { - nil - } - - package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] { - [] - } - - package func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] { - [] - } - - package func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport { - .full(RelatedFullDocumentDiagnosticReport(items: [])) - } - - package func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? { - nil - } - - package func documentRangeFormatting( - _ req: LanguageServerProtocol.DocumentRangeFormattingRequest - ) async throws -> [LanguageServerProtocol.TextEdit]? { - return nil - } - - package func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? { - return nil - } - - package func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) { - (edits: WorkspaceEdit(), usr: nil) - } - - package func editsToRename( - locations renameLocations: [RenameLocation], - in snapshot: DocumentSnapshot, - oldName: CrossLanguageName, - newName: CrossLanguageName - ) async throws -> [TextEdit] { - [] - } - - package func prepareRename( - _ request: PrepareRenameRequest - ) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? { - nil - } - - package func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit? { - nil - } - - package func editsToRenameParametersInFunctionBody( - snapshot: DocumentSnapshot, - renameLocation: RenameLocation, - newName: CrossLanguageName - ) async -> [TextEdit] { - [] - } - - package func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? { - nil - } - - package func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse { - GetReferenceDocumentResponse(content: "") - } - - package func syntacticDocumentTests( - for uri: DocumentURI, - in workspace: Workspace - ) async throws -> [AnnotatedTestItem]? { - nil - } - - package func canonicalDeclarationPosition( - of position: Position, - in uri: DocumentURI - ) async -> Position? { - nil - } - - package func crash() async { - // There's no way to crash the DocumentationLanguageService - } -} diff --git a/Sources/SourceKitLSP/LanguageServerType.swift b/Sources/SourceKitLSP/LanguageServerType.swift index 41796139f..a9b99eb79 100644 --- a/Sources/SourceKitLSP/LanguageServerType.swift +++ b/Sources/SourceKitLSP/LanguageServerType.swift @@ -17,7 +17,6 @@ import LanguageServerProtocol enum LanguageServerType: Hashable { case clangd case swift - case documentation init?(language: Language) { switch language { @@ -25,8 +24,6 @@ enum LanguageServerType: Hashable { self = .clangd case .swift: self = .swift - case .markdown, .tutorial: - self = .documentation default: return nil } @@ -47,8 +44,6 @@ enum LanguageServerType: Hashable { return ClangLanguageService.self case .swift: return SwiftLanguageService.self - case .documentation: - return DocumentationLanguageService.self } } } diff --git a/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift b/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift deleted file mode 100644 index d9607f56d..000000000 --- a/Tests/SourceKitLSPTests/DocumentationLanguageServiceTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 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 LanguageServerProtocol -import SKTestSupport -import SourceKitLSP -import XCTest - -final class DocumentationLanguageServiceTests: XCTestCase { - func testHandlesMarkdownFiles() async throws { - try await assertHandles(language: .markdown) - } - - func testHandlesTutorialFiles() async throws { - try await assertHandles(language: .tutorial) - } -} - -fileprivate func assertHandles(language: Language) async throws { - let testClient = try await TestSourceKitLSPClient() - let uri = DocumentURI(for: language) - testClient.openDocument("", uri: uri) - - // The DocumentationLanguageService doesn't do much right now except to enable handling `*.md` - // and `*.tutorial` files for the purposes of fulfilling documentation requests. We'll just - // issue a completion request here to make sure that an empty list is returned and that - // SourceKit-LSP does not respond with an error on requests for Markdown and Tutorial files. - let completions = try await testClient.send( - CompletionRequest(textDocument: .init(uri), position: .init(line: 0, utf16index: 0)) - ) - XCTAssertEqual(completions, .init(isIncomplete: false, items: [])) -} From fd79c3d21de4f5c527f7240746fa61fe4f89e644 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 10 Dec 2024 10:33:35 -0800 Subject: [PATCH 41/41] Revert "Merge pull request #1831 from woolsweater/the-trail-is-closed" This reverts commit 21dfaf0f9cff545e33c4d2910bf7698a41febe27, reversing changes made to f900b4ef2c1c9639971b8d1da8d3eea00de34889. --- Sources/SourceKitLSP/CMakeLists.txt | 1 - .../Swift/ClosureCompletionFormat.swift | 73 -------- .../Swift/CodeCompletionSession.swift | 11 +- .../Swift/RewriteSourceKitPlaceholders.swift | 170 ++---------------- .../ClosureCompletionFormatTests.swift | 130 -------------- .../RewriteSourceKitPlaceholdersTests.swift | 66 ------- .../SwiftCompletionTests.swift | 88 +++++---- 7 files changed, 80 insertions(+), 459 deletions(-) delete mode 100644 Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift delete mode 100644 Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift delete mode 100644 Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index da1e045f8..fdca6b335 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -26,7 +26,6 @@ target_sources(SourceKitLSP PRIVATE ) target_sources(SourceKitLSP PRIVATE Swift/AdjustPositionToStartOfIdentifier.swift - Swift/ClosureCompletionFormat.swift Swift/CodeActions/AddDocumentation.swift Swift/CodeActions/ConvertIntegerLiteral.swift Swift/CodeActions/ConvertJSONToCodableStruct.swift diff --git a/Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift b/Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift deleted file mode 100644 index ad628aced..000000000 --- a/Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift +++ /dev/null @@ -1,73 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 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 -// -//===----------------------------------------------------------------------===// - -#if compiler(>=6.0) -public import SwiftBasicFormat -public import SwiftSyntax -#else -import SwiftBasicFormat -import SwiftSyntax -#endif - -/// A specialization of `BasicFormat` for closure literals in a code completion -/// context. -/// -/// This is more conservative about newline insertion: unless the closure has -/// multiple statements in its body it will not be reformatted to multiple -/// lines. -@_spi(Testing) -public class ClosureCompletionFormat: BasicFormat { - @_spi(Testing) - public override func requiresNewline( - between first: TokenSyntax?, - and second: TokenSyntax? - ) -> Bool { - if let first, isEndOfSmallClosureSignature(first) { - return false - } else if let first, isSmallClosureDelimiter(first, kind: \.leftBrace) { - return false - } else if let second, isSmallClosureDelimiter(second, kind: \.rightBrace) { - return false - } else { - return super.requiresNewline(between: first, and: second) - } - } - - /// Returns `true` if `token` is an opening or closing brace (according to - /// `kind`) of a closure, and that closure has no more than one statement in - /// its body. - private func isSmallClosureDelimiter( - _ token: TokenSyntax, - kind: KeyPath - ) -> Bool { - guard token.keyPathInParent == kind, - let closure = token.parent?.as(ClosureExprSyntax.self) - else { - return false - } - - return closure.statements.count <= 1 - } - - /// Returns `true` if `token` is the last token in the signature of a closure, - /// and that closure has no more than one statement in its body. - private func isEndOfSmallClosureSignature(_ token: TokenSyntax) -> Bool { - guard - token.keyPathInParent == \ClosureSignatureSyntax.inKeyword, - let closure = token.ancestorOrSelf(mapping: { $0.as(ClosureExprSyntax.self) }) - else { - return false - } - - return closure.statements.count <= 1 - } -} diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index a573996dd..54b645379 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -323,14 +323,9 @@ class CodeCompletionSession { var parser = Parser(exprToExpand) let expr = ExprSyntax.parse(from: &parser) guard let call = OutermostFunctionCallFinder.findOutermostFunctionCall(in: expr), - let expandedCall = ExpandEditorPlaceholdersToLiteralClosures.refactor( + let expandedCall = ExpandEditorPlaceholdersToTrailingClosures.refactor( syntax: call, - in: ExpandEditorPlaceholdersToLiteralClosures.Context( - format: .custom( - ClosureCompletionFormat(indentationWidth: indentationWidth), - allowNestedPlaceholders: true - ) - ) + in: ExpandEditorPlaceholdersToTrailingClosures.Context(indentationWidth: indentationWidth) ) else { return nil @@ -339,7 +334,7 @@ class CodeCompletionSession { let bytesToExpand = Array(exprToExpand.utf8) var expandedBytes: [UInt8] = [] - // Add the prefix that we stripped off to allow expression parsing + // Add the prefix that we stripped of to allow expression parsing expandedBytes += strippedPrefix.utf8 // Add any part of the expression that didn't end up being part of the function call expandedBytes += bytesToExpand[0..` — in `input` to LSP -/// placeholder syntax: `${n:foo}`. -/// -/// If `clientSupportsSnippets` is `false`, the placeholder is rendered as an -/// empty string, to prevent the client from inserting special placeholder -/// characters as if they were literal text. -@_spi(Testing) -public func rewriteSourceKitPlaceholders(in input: String, clientSupportsSnippets: Bool) -> String { - var result = "" - var nextPlaceholderNumber = 1 - // Current stack of nested placeholders, most nested last. Each element needs - // to be rendered inside the element before it. - var placeholders: [(number: Int, contents: String)] = [] - let tokens = tokenize(input) - for token in tokens { - switch token { - case let .text(text): - if placeholders.isEmpty { - result += text - } else { - placeholders.latest.contents += text - } - - case let .curlyBrace(brace): - if placeholders.isEmpty { - result.append(brace) - } else { - // Braces are only escaped _inside_ a placeholder; otherwise the client - // would include the backslashes literally. - placeholders.latest.contents.append(contentsOf: ["\\", brace]) - } - - case .placeholderOpen: - placeholders.append((number: nextPlaceholderNumber, contents: "")) - nextPlaceholderNumber += 1 - - case .placeholderClose: - guard let (number, placeholderBody) = placeholders.popLast() else { - logger.fault("Invalid placeholder in \(input)") - return input - } - guard let displayName = nameForSnippet(placeholderBody) else { - logger.fault("Failed to decode placeholder \(placeholderBody) in \(input)") - return input - } - let placeholder = - clientSupportsSnippets - ? formatLSPPlaceholder(displayName, number: number) - : "" - if placeholders.isEmpty { - result += placeholder - } else { - placeholders.latest.contents += placeholder - } - } - } - - return result -} - -/// Scan `input` to identify special elements within: curly braces, which may -/// need to be escaped; and SourceKit placeholder open/close delimiters. -private func tokenize(_ input: String) -> [SnippetToken] { - var index = input.startIndex - var isAtEnd: Bool { index == input.endIndex } - func match(_ char: Character) -> Bool { - if isAtEnd || input[index] != char { - return false - } else { - input.formIndex(after: &index) - return true +func rewriteSourceKitPlaceholders(in string: String, clientSupportsSnippets: Bool) -> String { + var result = string + var index = 1 + while let start = result.range(of: "<#") { + guard let end = result[start.upperBound...].range(of: "#>") else { + logger.fault("Invalid placeholder in \(string)") + return string } - } - func next() -> Character? { - guard !isAtEnd else { return nil } - defer { input.formIndex(after: &index) } - return input[index] - } - - var tokens: [SnippetToken] = [] - var text = "" - while let char = next() { - switch char { - case "<": - if match("#") { - tokens.append(.text(text)) - text.removeAll() - tokens.append(.placeholderOpen) - } else { - text.append(char) - } - - case "#": - if match(">") { - tokens.append(.text(text)) - text.removeAll() - tokens.append(.placeholderClose) - } else { - text.append(char) - } - - case "{", "}": - tokens.append(.text(text)) - text.removeAll() - tokens.append(.curlyBrace(char)) - - case let c: - text.append(c) + let rawPlaceholder = String(result[start.lowerBound.. String? { - var text = rewrappedAsPlaceholder(body) +/// Parse a SourceKit placeholder and extract the display name suitable for a +/// LSP snippet. +fileprivate func nameForSnippet(_ text: String) -> String? { + var text = text return text.withSyntaxText { guard let data = RawEditorPlaceholderData(syntaxText: $0) else { return nil @@ -152,28 +45,3 @@ private func nameForSnippet(_ body: String) -> String? { return String(syntaxText: data.typeForExpansionText ?? data.displayText) } } - -private let placeholderStart = "<#" -private let placeholderEnd = "#>" -private func rewrappedAsPlaceholder(_ body: String) -> String { - return placeholderStart + body + placeholderEnd -} - -/// Wrap `body` in LSP snippet placeholder syntax, using `number` as the -/// placeholder's index in the snippet. -private func formatLSPPlaceholder(_ body: String, number: Int) -> String { - "${\(number):\(body)}" -} - -private extension Array { - /// Mutable access to the final element of an array. - /// - /// - precondition: The array must not be empty. - var latest: Element { - get { self.last! } - _modify { - let index = self.index(before: self.endIndex) - yield &self[index] - } - } -} diff --git a/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift b/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift deleted file mode 100644 index 6ec213001..000000000 --- a/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift +++ /dev/null @@ -1,130 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 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 -// -//===----------------------------------------------------------------------===// - -@_spi(Testing) import SourceKitLSP -import Swift -import SwiftBasicFormat -import SwiftParser -import SwiftSyntax -import SwiftSyntaxBuilder -import XCTest - -fileprivate func assertFormatted( - tree: T, - expected: String, - using format: ClosureCompletionFormat = ClosureCompletionFormat(indentationWidth: .spaces(4)), - file: StaticString = #filePath, - line: UInt = #line -) { - XCTAssertEqual(tree.formatted(using: format).description, expected, file: file, line: line) -} - -fileprivate func assertFormatted( - source: String, - expected: String, - using format: ClosureCompletionFormat = ClosureCompletionFormat(indentationWidth: .spaces(4)), - file: StaticString = #filePath, - line: UInt = #line -) { - assertFormatted( - tree: Parser.parse(source: source), - expected: expected, - using: format, - file: file, - line: line - ) -} - -final class ClosureCompletionFormatTests: XCTestCase { - func testSingleStatementClosureArg() { - assertFormatted( - source: """ - foo(bar: { baz in baz.quux }) - """, - expected: """ - foo(bar: { baz in baz.quux }) - """ - ) - } - - func testSingleStatementClosureArgAlreadyMultiLine() { - assertFormatted( - source: """ - foo( - bar: { baz in - baz.quux - } - ) - """, - expected: """ - foo( - bar: { baz in - baz.quux - } - ) - """ - ) - } - - func testMultiStatmentClosureArg() { - assertFormatted( - source: """ - foo( - bar: { baz in print(baz); return baz.quux } - ) - """, - expected: """ - foo( - bar: { baz in - print(baz); - return baz.quux - } - ) - """ - ) - } - - func testMultiStatementClosureArgAlreadyMultiLine() { - assertFormatted( - source: """ - foo( - bar: { baz in - print(baz) - return baz.quux - } - ) - """, - expected: """ - foo( - bar: { baz in - print(baz) - return baz.quux - } - ) - """ - ) - } - - func testFormatClosureWithInitialIndentation() throws { - assertFormatted( - tree: ClosureExprSyntax( - statements: CodeBlockItemListSyntax([ - CodeBlockItemSyntax(item: CodeBlockItemSyntax.Item(IntegerLiteralExprSyntax(integerLiteral: 2))) - ]) - ), - expected: """ - { 2 } - """, - using: ClosureCompletionFormat(initialIndentation: .spaces(4)) - ) - } -} diff --git a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift deleted file mode 100644 index c2131cbf0..000000000 --- a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 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 SKTestSupport -@_spi(Testing) import SourceKitLSP -import XCTest - -final class RewriteSourceKitPlaceholdersTests: XCTestCase { - func testClientDoesNotSupportSnippets() { - let input = "foo(bar: <#T##Int##Int#>)" - let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: false) - - XCTAssertEqual(rewritten, "foo(bar: )") - } - - func testInputWithoutPlaceholders() { - let input = "foo()" - let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - - XCTAssertEqual(rewritten, "foo()") - } - - func testPlaceholderWithType() { - let input = "foo(bar: <#T##bar##Int#>)" - let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - - XCTAssertEqual(rewritten, "foo(bar: ${1:Int})") - } - - func testMultiplePlaceholders() { - let input = "foo(bar: <#T##Int##Int#>, baz: <#T##String##String#>, quux: <#T##String##String#>)" - let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - - XCTAssertEqual(rewritten, "foo(bar: ${1:Int}, baz: ${2:String}, quux: ${3:String})") - } - - func testClosurePlaceholderReturnType() { - let input = "foo(bar: <#{ <#T##Int##Int#> }#>)" - let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - - XCTAssertEqual(rewritten, #"foo(bar: ${1:\{ ${2:Int} \}})"#) - } - - func testClosurePlaceholderArgumentType() { - let input = "foo(bar: <#{ <#T##Int##Int#> in <#T##Void##Void#> }#>)" - let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - - XCTAssertEqual(rewritten, #"foo(bar: ${1:\{ ${2:Int} in ${3:Void} \}})"#) - } - - func testMultipleClosurePlaceholders() { - let input = "foo(<#{ <#T##Int##Int#> }#>, baz: <#{ <#Int#> in <#T##Bool##Bool#> }#>)" - let rewritten = rewriteSourceKitPlaceholders(in: input, clientSupportsSnippets: true) - - XCTAssertEqual(rewritten, #"foo(${1:\{ ${2:Int} \}}, baz: ${3:\{ ${4:Int} in ${5:Bool} \}})"#) - } -} diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 25fa3f7f0..4ac1f2baf 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -863,16 +863,20 @@ final class SwiftCompletionTests: XCTestCase { deprecated: false, sortText: nil, filterText: "myMap(:)", - insertText: #""" - myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) - """#, + insertText: """ + myMap { ${1:Int} in + ${2:Bool} + } + """, insertTextFormat: .snippet, textEdit: .textEdit( TextEdit( range: Range(positions["1️⃣"]), - newText: #""" - myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) - """# + newText: """ + myMap { ${1:Int} in + ${2:Bool} + } + """ ) ) ) @@ -907,16 +911,20 @@ final class SwiftCompletionTests: XCTestCase { deprecated: false, sortText: nil, filterText: ".myMap(:)", - insertText: #""" - ?.myMap(${1:\{ ${2:Int} in ${3:Bool} \}}) - """#, + insertText: """ + ?.myMap { ${1:Int} in + ${2:Bool} + } + """, insertTextFormat: .snippet, textEdit: .textEdit( TextEdit( range: positions["1️⃣"]..