diff --git a/Package.swift b/Package.swift index af07489dd..d70271ecb 100644 --- a/Package.swift +++ b/Package.swift @@ -267,6 +267,7 @@ var targets: [Target] = [ "SKLogging", "SKOptions", "SourceKitLSP", + "SwiftLanguageService", "ToolchainRegistry", "TSCExtensions", ], @@ -464,6 +465,7 @@ var targets: [Target] = [ "SourceKitD", "SourceKitLSP", "SwiftExtensions", + "SwiftLanguageService", "ToolchainRegistry", "TSCExtensions", .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), @@ -533,17 +535,8 @@ var targets: [Target] = [ "ToolchainRegistry", "TSCExtensions", .product(name: "IndexStoreDB", package: "indexstore-db"), - .product(name: "Crypto", package: "swift-crypto"), .product(name: "Markdown", package: "swift-markdown"), - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ] - + swiftPMDependency([ - .product(name: "SwiftPM-auto", package: "swift-package-manager") - ]) - + swiftSyntaxDependencies([ - "SwiftBasicFormat", "SwiftDiagnostics", "SwiftIDEUtils", "SwiftParser", "SwiftParserDiagnostics", - "SwiftRefactor", "SwiftSyntax", - ]), + ] + swiftSyntaxDependencies(["SwiftSyntax"]), exclude: ["CMakeLists.txt"], swiftSettings: globalSwiftSettings ), @@ -597,6 +590,48 @@ var targets: [Target] = [ swiftSettings: globalSwiftSettings ), + // MARK: SwiftLanguageService + + .target( + name: "SwiftLanguageService", + dependencies: [ + "BuildServerProtocol", + "BuildServerIntegration", + "Csourcekitd", + "DocCDocumentation", + "LanguageServerProtocol", + "LanguageServerProtocolExtensions", + "LanguageServerProtocolJSONRPC", + "SemanticIndex", + "SKLogging", + "SKOptions", + "SKUtilities", + "SourceKitD", + "SourceKitLSP", + "SwiftExtensions", + "ToolchainRegistry", + "TSCExtensions", + .product(name: "IndexStoreDB", package: "indexstore-db"), + .product(name: "Crypto", package: "swift-crypto"), + .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), + ] + + swiftPMDependency([ + .product(name: "SwiftPM-auto", package: "swift-package-manager") + ]) + + swiftSyntaxDependencies([ + "SwiftBasicFormat", + "SwiftDiagnostics", + "SwiftIDEUtils", + "SwiftParser", + "SwiftParserDiagnostics", + "SwiftRefactor", + "SwiftSyntax", + "SwiftSyntaxBuilder", + ]), + exclude: ["CMakeLists.txt"], + swiftSettings: globalSwiftSettings + ), + // MARK: SwiftSourceKitClientPlugin .target( diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 6d532df1f..8460311a1 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -1,8 +1,8 @@ add_compile_options("$<$:SHELL:-package-name sourcekit_lsp>") add_compile_options("$<$:SHELL:-DRESILIENT_LIBRARIES>") add_compile_options("$<$:SHELL:-swift-version 6>") -add_subdirectory(BuildServerProtocol) add_subdirectory(BuildServerIntegration) +add_subdirectory(BuildServerProtocol) add_subdirectory(CAtomics) add_subdirectory(CCompletionScoring) add_subdirectory(ClangLanguageService) @@ -17,10 +17,11 @@ add_subdirectory(SemanticIndex) add_subdirectory(SKLogging) add_subdirectory(SKOptions) add_subdirectory(SKUtilities) -add_subdirectory(SourceKitLSP) -add_subdirectory(SourceKitD) add_subdirectory(sourcekit-lsp) +add_subdirectory(SourceKitD) +add_subdirectory(SourceKitLSP) add_subdirectory(SwiftExtensions) +add_subdirectory(SwiftLanguageService) add_subdirectory(SwiftSourceKitClientPlugin) add_subdirectory(SwiftSourceKitPlugin) add_subdirectory(SwiftSourceKitPluginCommon) diff --git a/Sources/ClangLanguageService/ClangLanguageService.swift b/Sources/ClangLanguageService/ClangLanguageService.swift index b61466168..70a26e336 100644 --- a/Sources/ClangLanguageService/ClangLanguageService.swift +++ b/Sources/ClangLanguageService/ClangLanguageService.swift @@ -12,6 +12,7 @@ import BuildServerIntegration import Foundation +package import IndexStoreDB package import LanguageServerProtocol import LanguageServerProtocolExtensions import LanguageServerProtocolJSONRPC @@ -100,7 +101,7 @@ package actor ClangLanguageService: LanguageService, MessageHandler { /// The documents that have been opened and which language they have been /// opened with. - private var openDocuments: [DocumentURI: Language] = [:] + private var openDocuments: [DocumentURI: LanguageServerProtocol.Language] = [:] /// Type to map `clangd`'s semantic token legend to SourceKit-LSP's. private var semanticTokensTranslator: SemanticTokensLegendTranslator? = nil @@ -108,6 +109,8 @@ package actor ClangLanguageService: LanguageService, MessageHandler { /// While `clangd` is running, its `Process` object. private var clangdProcess: Process? + package static var builtInCommands: [String] { [] } + /// Creates a language server for the given client referencing the clang binary specified in `toolchain`. /// Returns `nil` if `clangd` can't be found. package init?( @@ -500,7 +503,7 @@ extension ClangLanguageService { throw ResponseError.unknown("Connection to the editor closed") } - let snapshot = try await sourceKitLSPServer.documentManager.latestSnapshot(req.textDocument.uri) + let snapshot = try sourceKitLSPServer.documentManager.latestSnapshot(req.textDocument.uri) throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language)) } #endif @@ -509,6 +512,13 @@ extension ClangLanguageService { return try await forwardRequestToClangd(req) } + package func symbolGraph( + forOnDiskContentsOf symbolDocumentUri: DocumentURI, + at location: SymbolLocation + ) async throws -> String? { + return nil + } + package func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? { return try await forwardRequestToClangd(req) } @@ -652,6 +662,10 @@ extension ClangLanguageService { return nil } + package static func syntacticTestItems(in uri: DocumentURI) async -> [AnnotatedTestItem] { + return [] + } + package func editsToRename( locations renameLocations: [RenameLocation], in snapshot: DocumentSnapshot, diff --git a/Sources/InProcessClient/CMakeLists.txt b/Sources/InProcessClient/CMakeLists.txt index dd9399d36..1c8646ff1 100644 --- a/Sources/InProcessClient/CMakeLists.txt +++ b/Sources/InProcessClient/CMakeLists.txt @@ -12,6 +12,7 @@ target_link_libraries(InProcessClient PUBLIC SKLogging SKOptions SourceKitLSP + SwiftLanguageService ToolchainRegistry ) diff --git a/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift b/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift index a5a44d44f..be37bec31 100644 --- a/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift +++ b/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift @@ -13,6 +13,7 @@ import ClangLanguageService import LanguageServerProtocol package import SourceKitLSP +import SwiftLanguageService extension LanguageServiceRegistry { /// All types conforming to `LanguageService` that are known at compile time. diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index b3c3a7a46..b4ece6593 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -18,6 +18,7 @@ import SKLogging import SourceKitD import SourceKitLSP import SwiftExtensions +import SwiftLanguageService import TSCExtensions import ToolchainRegistry import XCTest @@ -378,7 +379,7 @@ package actor SkipUnless { let tokens = SyntaxHighlightingTokens(lspEncodedTokens: response.data) return tokens.tokens.last - == SourceKitLSP.SyntaxHighlightingToken( + == SyntaxHighlightingToken( range: Position(line: 1, utf16index: 4)..>:FoundationXML>) + $<$>:FoundationXML> +) diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index 118758c1e..362cf6dc4 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -20,7 +20,7 @@ import SwiftExtensions /// capabilities. package final actor CapabilityRegistry { /// The client's capabilities as they were reported when sourcekit-lsp was launched. - package let clientCapabilities: ClientCapabilities + package nonisolated let clientCapabilities: ClientCapabilities // MARK: Tracking capabilities dynamically registered in the client diff --git a/Sources/SourceKitLSP/DocumentSnapshot+PositionConversions.swift b/Sources/SourceKitLSP/DocumentSnapshot+PositionConversions.swift index a4be6a9bc..b8df99d9d 100644 --- a/Sources/SourceKitLSP/DocumentSnapshot+PositionConversions.swift +++ b/Sources/SourceKitLSP/DocumentSnapshot+PositionConversions.swift @@ -237,23 +237,6 @@ extension DocumentSnapshot { ) } - // MARK: Position <-> SourceKitDPosition - - func sourcekitdPosition( - of position: Position, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> SourceKitDPosition { - let utf8Column = lineTable.utf8ColumnAt( - line: position.line, - utf16Column: position.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - // FIXME: Introduce new type for UTF-8 based positions - return SourceKitDPosition(line: position.line + 1, utf8Column: utf8Column + 1) - } - // MAR: Position <-> SymbolLocation /// Converts the given UTF-8-offset-based `SymbolLocation` to a UTF-16-based line:column `Position`. diff --git a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift index 7440221f4..a2ccaa16f 100644 --- a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift +++ b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift @@ -64,22 +64,10 @@ extension DocumentationLanguageService { fetchSymbolGraph: { location in guard let symbolWorkspace = try await workspaceForDocument(uri: location.documentUri), let languageService = try await languageService(for: location.documentUri, .swift, in: symbolWorkspace) - as? SwiftLanguageService else { throw ResponseError.internalError("Unable to find Swift language service for \(location.documentUri)") } - return try await languageService.withSnapshotFromDiskOpenedInSourcekitd( - uri: location.documentUri, - fallbackSettingsAfterTimeout: false - ) { (snapshot, compileCommand) in - let (_, _, symbolGraph) = try await languageService.cursorInfo( - snapshot, - compileCommand: compileCommand, - Range(snapshot.position(of: location)), - includeSymbolGraph: true - ) - return symbolGraph - } + return try await languageService.symbolGraph(forOnDiskContentsOf: location.documentUri, at: location) } ) else { @@ -89,21 +77,13 @@ extension DocumentationLanguageService { guard let symbolWorkspace = try await workspaceForDocument(uri: symbolDocumentUri), let languageService = try await languageService(for: symbolDocumentUri, .swift, in: symbolWorkspace) - as? SwiftLanguageService else { throw ResponseError.internalError("Unable to find Swift language service for \(symbolDocumentUri)") } - let symbolGraph = try await languageService.withSnapshotFromDiskOpenedInSourcekitd( - uri: symbolDocumentUri, - fallbackSettingsAfterTimeout: false - ) { snapshot, compileCommand in - try await languageService.cursorInfo( - snapshot, - compileCommand: compileCommand, - Range(snapshot.position(of: symbolOccurrence.location)), - includeSymbolGraph: true - ).symbolGraph - } + let symbolGraph = try await languageService.symbolGraph( + forOnDiskContentsOf: symbolDocumentUri, + at: symbolOccurrence.location + ) guard let symbolGraph else { throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)") } diff --git a/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift b/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift index aac9d79f9..6999a4ab3 100644 --- a/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift +++ b/Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation +package import IndexStoreDB package import LanguageServerProtocol package import SKOptions import SwiftExtensions @@ -30,6 +31,8 @@ package actor DocumentationLanguageService: LanguageService, Sendable { } } + package static var builtInCommands: [String] { [] } + package init?( sourceKitLSPServer: SourceKitLSPServer, toolchain: Toolchain, @@ -140,6 +143,13 @@ package actor DocumentationLanguageService: LanguageService, Sendable { [] } + package func symbolGraph( + forOnDiskContentsOf symbolDocumentUri: DocumentURI, + at location: SymbolLocation + ) async throws -> String? { + return nil + } + package func openGeneratedInterface( document: DocumentURI, moduleName: String, @@ -271,6 +281,10 @@ package actor DocumentationLanguageService: LanguageService, Sendable { nil } + package static func syntacticTestItems(in uri: DocumentURI) async -> [AnnotatedTestItem] { + return [] + } + package func canonicalDeclarationPosition( of position: Position, in uri: DocumentURI diff --git a/Sources/SourceKitLSP/Swift/GeneratedInterfaceDocumentURLData.swift b/Sources/SourceKitLSP/GeneratedInterfaceDocumentURLData.swift similarity index 88% rename from Sources/SourceKitLSP/Swift/GeneratedInterfaceDocumentURLData.swift rename to Sources/SourceKitLSP/GeneratedInterfaceDocumentURLData.swift index bfa6522df..5e90b35ef 100644 --- a/Sources/SourceKitLSP/Swift/GeneratedInterfaceDocumentURLData.swift +++ b/Sources/SourceKitLSP/GeneratedInterfaceDocumentURLData.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation -import LanguageServerProtocol +package import LanguageServerProtocol /// Represents url of generated interface reference document. package struct GeneratedInterfaceDocumentURLData: Hashable, ReferenceURLData { @@ -25,18 +25,18 @@ package struct GeneratedInterfaceDocumentURLData: Hashable, ReferenceURLData { } /// The module that should be shown in this generated interface. - let moduleName: String + package let moduleName: String /// The group that should be shown in this generated interface, if applicable. - let groupName: String? + package let groupName: String? /// The name by which this document is referred to in sourcekitd. - let sourcekitdDocumentName: String + package let sourcekitdDocumentName: String /// The document from which the build settings for the generated interface should be inferred. - let buildSettingsFrom: DocumentURI + package let buildSettingsFrom: DocumentURI - var displayName: String { + package var displayName: String { if let groupName { return "\(moduleName).\(groupName.replacing("/", with: ".")).swiftinterface" } @@ -57,13 +57,13 @@ package struct GeneratedInterfaceDocumentURLData: Hashable, ReferenceURLData { return result } - var uri: DocumentURI { + package var uri: DocumentURI { get throws { try ReferenceDocumentURL.generatedInterface(self).uri } } - init(moduleName: String, groupName: String?, sourcekitdDocumentName: String, primaryFile: DocumentURI) { + package init(moduleName: String, groupName: String?, sourcekitdDocumentName: String, primaryFile: DocumentURI) { self.moduleName = moduleName self.groupName = groupName self.sourcekitdDocumentName = sourcekitdDocumentName diff --git a/Sources/SourceKitLSP/LanguageService.swift b/Sources/SourceKitLSP/LanguageService.swift index fce4e3f43..924d92468 100644 --- a/Sources/SourceKitLSP/LanguageService.swift +++ b/Sources/SourceKitLSP/LanguageService.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation +package import IndexStoreDB package import LanguageServerProtocol package import SKOptions package import SwiftSyntax @@ -47,7 +48,7 @@ package struct RenameLocation: Sendable { /// /// This is primarily used to influence how argument labels should be renamed in Swift and if a location should be /// rejected if argument labels don't match. - enum Usage { + package enum Usage { /// The definition of a function/subscript/variable/... case definition @@ -70,12 +71,18 @@ package struct RenameLocation: Sendable { } /// The line of the identifier to be renamed (1-based). - let line: Int + package let line: Int /// The column of the identifier to be renamed in UTF-8 bytes (1-based). - let utf8Column: Int + package let utf8Column: Int - let usage: Usage + package let usage: Usage + + package init(line: Int, utf8Column: Int, usage: RenameLocation.Usage) { + self.line = line + self.utf8Column = utf8Column + self.usage = usage + } } /// The textual output of a module interface. @@ -111,6 +118,9 @@ package protocol LanguageService: AnyObject, Sendable { /// If this returns `false`, a new language server will be started for `workspace`. func canHandle(workspace: Workspace, toolchain: Toolchain) -> Bool + /// Identifiers of the commands that this language service can handle. + static var builtInCommands: [String] { get } + // MARK: - Lifetime func initialize(_ initialize: InitializeRequest) async throws -> InitializeResult @@ -171,6 +181,13 @@ package protocol LanguageService: AnyObject, Sendable { #endif func symbolInfo(_ request: SymbolInfoRequest) async throws -> [SymbolDetails] + /// Return the symbol graph at the given location for the contents of the document as they are on-disk (opposed to the + /// in-memory modified version of the document). + func symbolGraph( + forOnDiskContentsOf symbolDocumentUri: DocumentURI, + at location: SymbolLocation + ) async throws -> String? + /// Request a generated interface of a module to display in the IDE. /// /// - Parameters: @@ -268,6 +285,13 @@ package protocol LanguageService: AnyObject, Sendable { /// A return value of `nil` indicates that this language service does not support syntactic test discovery. func syntacticDocumentTests(for uri: DocumentURI, in workspace: Workspace) async throws -> [AnnotatedTestItem]? + /// Syntactically scans the file at the given URL for tests declared within it. + /// + /// Does not write the results to the index. + /// + /// The order of the returned tests is not defined. The results should be sorted before being returned to the editor. + static func syntacticTestItems(in uri: DocumentURI) async -> [AnnotatedTestItem] + /// A position that is canonical for all positions within a declaration. For example, if we have the following /// declaration, then all `|` markers should return the same canonical position. /// ``` diff --git a/Sources/SourceKitLSP/LanguageServiceRegistry.swift b/Sources/SourceKitLSP/LanguageServiceRegistry.swift index c931f707b..0d7c0aca4 100644 --- a/Sources/SourceKitLSP/LanguageServiceRegistry.swift +++ b/Sources/SourceKitLSP/LanguageServiceRegistry.swift @@ -14,6 +14,23 @@ import IndexStoreDB import LanguageServerProtocol import SKLogging +/// Wrapper around `LanguageService.Type`, making it conform to `Hashable`. +struct LanguageServiceType: Hashable { + let type: LanguageService.Type + + init(_ type: any LanguageService.Type) { + self.type = type + } + + static func == (lhs: LanguageServiceType, rhs: LanguageServiceType) -> Bool { + return ObjectIdentifier(lhs.type) == ObjectIdentifier(rhs.type) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(type)) + } +} + /// Registry in which conformers to `LanguageService` can be registered to server semantic functionality for a set of /// languages. package struct LanguageServiceRegistry { @@ -36,4 +53,8 @@ package struct LanguageServiceRegistry { func languageService(for language: Language) -> LanguageService.Type? { return byLanguage[language] } + + var languageServices: Set { + return Set(byLanguage.values.map { LanguageServiceType($0) }) + } } diff --git a/Sources/SourceKitLSP/Swift/MacroExpansionReferenceDocumentURLData.swift b/Sources/SourceKitLSP/MacroExpansionReferenceDocumentURLData.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/MacroExpansionReferenceDocumentURLData.swift rename to Sources/SourceKitLSP/MacroExpansionReferenceDocumentURLData.swift diff --git a/Sources/SourceKitLSP/Swift/ReferenceDocumentURL.swift b/Sources/SourceKitLSP/ReferenceDocumentURL.swift similarity index 96% rename from Sources/SourceKitLSP/Swift/ReferenceDocumentURL.swift rename to Sources/SourceKitLSP/ReferenceDocumentURL.swift index 8b5d86320..bcf19c5b4 100644 --- a/Sources/SourceKitLSP/Swift/ReferenceDocumentURL.swift +++ b/Sources/SourceKitLSP/ReferenceDocumentURL.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation -import LanguageServerProtocol +package import LanguageServerProtocol protocol ReferenceURLData { static var documentType: String { get } @@ -58,13 +58,13 @@ package enum ReferenceDocumentURL { } } - var uri: DocumentURI { + package var uri: DocumentURI { get throws { DocumentURI(try url) } } - init(from uri: DocumentURI) throws { + package init(from uri: DocumentURI) throws { try self.init(from: uri.arbitrarySchemeURL) } @@ -139,7 +139,7 @@ extension DocumentURI { /// /// For normal document URIs, this is the pseudo path of this URI. For macro expansions, this is the buffer name /// that the URI references. - var sourcekitdSourceFile: String { + package var sourcekitdSourceFile: String { if let referenceDocument = try? ReferenceDocumentURL(from: self) { referenceDocument.sourcekitdSourceFile } else { @@ -152,7 +152,7 @@ extension DocumentURI { /// /// The primary file is used to determine the workspace and language service that is used to generate the reference /// document as well as getting the reference document's build settings. - var primaryFile: DocumentURI? { + package var primaryFile: DocumentURI? { if let referenceDocument = try? ReferenceDocumentURL(from: self) { return referenceDocument.primaryFile } @@ -160,7 +160,7 @@ extension DocumentURI { } /// The file that should be used to retrieve build settings for this reference document. - var buildSettingsFile: DocumentURI { + package var buildSettingsFile: DocumentURI { if let referenceDocument = try? ReferenceDocumentURL(from: self) { return referenceDocument.buildSettingsFile } diff --git a/Sources/SourceKitLSP/Rename.swift b/Sources/SourceKitLSP/Rename.swift index 9421785e0..8faf5e233 100644 --- a/Sources/SourceKitLSP/Rename.swift +++ b/Sources/SourceKitLSP/Rename.swift @@ -10,284 +10,15 @@ // //===----------------------------------------------------------------------===// -import Csourcekitd -import Foundation -import IndexStoreDB +package import IndexStoreDB package import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging -import SKUtilities import SemanticIndex -import SourceKitD import SwiftExtensions -import SwiftSyntax // MARK: - Helper types -/// A parsed representation of a name that may be disambiguated by its argument labels. -/// -/// ### Examples -/// - `foo(a:b:)` -/// - `foo(_:b:)` -/// - `foo` if no argument labels are specified, eg. for a variable. -private struct CompoundDeclName { - /// The parameter of a compound decl name, which can either be the parameter's name or `_` to indicate that the - /// parameter is unnamed. - enum Parameter: Equatable { - case named(String) - case wildcard - - var stringOrWildcard: String { - switch self { - case .named(let str): return str - case .wildcard: return "_" - } - } - - var stringOrEmpty: String { - switch self { - case .named(let str): return str - case .wildcard: return "" - } - } - } - - let baseName: String - let parameters: [Parameter] - - /// Parse a compound decl name into its base names and parameters. - init(_ compoundDeclName: String) { - guard let openParen = compoundDeclName.firstIndex(of: "(") else { - // We don't have a compound name. Everything is the base name - self.baseName = compoundDeclName - self.parameters = [] - return - } - self.baseName = String(compoundDeclName[.. Int` - case keywordBaseName - - /// The internal parameter name (aka. second name) inside a function declaration - /// - /// ### Examples - /// - ` b` in `func foo(a b: Int)` - case parameterName - - /// Same as `parameterName` but cannot be removed if it is the same as the parameter's first name. This only happens - /// for subscripts where parameters are unnamed by default unless they have both a first and second name. - /// - /// ### Examples - /// The second ` a` in `subscript(a a: Int)` - case noncollapsibleParameterName - - /// The external argument label of a function parameter - /// - /// ### Examples - /// - `a` in `func foo(a b: Int)` - /// - `a` in `func foo(a: Int)` - case declArgumentLabel - - /// The argument label inside a call. - /// - /// ### Examples - /// - `a` in `foo(a: 1)` - case callArgumentLabel - - /// The colon after an argument label inside a call. This is reported so it can be removed if the parameter becomes - /// unnamed. - /// - /// ### Examples - /// - `: ` in `foo(a: 1)` - case callArgumentColon - - /// An empty range that point to the position before an unnamed argument. This is used to insert the argument label - /// if an unnamed parameter becomes named. - /// - /// ### Examples - /// - An empty range before `1` in `foo(1)`, which could expand to `foo(a: 1)` - case callArgumentCombined - - /// The argument label in a compound decl name. - /// - /// ### Examples - /// - `a` in `foo(a:)` - case selectorArgumentLabel - - init?(_ uid: sourcekitd_api_uid_t, values: sourcekitd_api_values) { - switch uid { - case values.renameRangeBase: self = .baseName - case values.renameRangeCallArgColon: self = .callArgumentColon - case values.renameRangeCallArgCombined: self = .callArgumentCombined - case values.renameRangeCallArgLabel: self = .callArgumentLabel - case values.renameRangeDeclArgLabel: self = .declArgumentLabel - case values.renameRangeKeywordBase: self = .keywordBaseName - case values.renameRangeNoncollapsibleParam: self = .noncollapsibleParameterName - case values.renameRangeParam: self = .parameterName - case values.renameRangeSelectorArgLabel: self = .selectorArgumentLabel - default: return nil - } - } -} - -/// A single “piece” that is used for renaming a compound function name. -/// -/// See `SyntacticRenamePieceKind` for the different rename pieces that exist. -/// -/// ### Example -/// `foo(x: 1)` is represented by three pieces -/// - The base name `foo` -/// - The parameter name `x` -/// - The call argument colon `: `. -private struct SyntacticRenamePiece { - /// The range that represents this piece of the name - let range: Range - - /// The kind of the rename piece. - let kind: SyntacticRenamePieceKind - - /// If this piece belongs to a parameter, the index of that parameter (zero-based) or `nil` if this is the base name - /// piece. - let parameterIndex: Int? - - /// Create a `SyntacticRenamePiece` from a `sourcekitd` response. - init?( - _ dict: SKDResponseDictionary, - in snapshot: DocumentSnapshot, - keys: sourcekitd_api_keys, - values: sourcekitd_api_values - ) { - guard let line: Int = dict[keys.line], - let column: Int = dict[keys.column], - let endLine: Int = dict[keys.endLine], - let endColumn: Int = dict[keys.endColumn], - let kind: sourcekitd_api_uid_t = dict[keys.kind] - else { - return nil - } - let start = snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: column - 1) - let end = snapshot.positionOf(zeroBasedLine: endLine - 1, utf8Column: endColumn - 1) - guard let kind = SyntacticRenamePieceKind(kind, values: values) else { - return nil - } - - self.range = start.., callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Substring { - let start = self.stringIndexOf( - line: range.lowerBound.line, - utf16Column: range.lowerBound.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - let end = self.stringIndexOf( - line: range.upperBound.line, - utf16Column: range.upperBound.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - return self.content[start.. String { - guard let snapshot = try? documentManager.latestSnapshotOrDisk(uri, language: .swift) else { - throw ResponseError.unknown("Failed to get contents of \(uri.forLogging) to translate Swift name to clang name") - } - - let req = sourcekitd.dictionary([ - keys.sourceFile: snapshot.uri.pseudoPath, - keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs - as [SKDRequestValue]?, - keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)), - keys.nameKind: sourcekitd.values.nameSwift, - keys.baseName: name.baseName, - keys.argNames: sourcekitd.array(name.parameters.map { $0.stringOrWildcard }), - ]) - - let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) - - guard let isZeroArgSelector: Int = response[keys.isZeroArgSelector], - let selectorPieces: SKDResponseArray = response[keys.selectorPieces] - else { - throw NameTranslationError.malformedSwiftToClangTranslateNameResponse(response) - } - return - try selectorPieces - .map { (dict: SKDResponseDictionary) -> String in - guard var name: String = dict[keys.name] else { - throw NameTranslationError.malformedSwiftToClangTranslateNameResponse(response) - } - if isZeroArgSelector == 0 { - // Selector pieces in multi-arg selectors end with ":" - name.append(":") - } - return name - }.joined() - } - - /// Translates a C/C++/Objective-C symbol name to Swift. - /// - /// This requires the position at which the the symbol is referenced in Swift so sourcekitd can determine the - /// clang declaration that is being renamed and check if that declaration has a `SWIFT_NAME`. If it does, this - /// `SWIFT_NAME` is used as the name translation result instead of invoking the clang importer rename rules. - /// - /// - Parameters: - /// - position: A position at which this symbol is referenced from Swift. - /// - snapshot: The snapshot containing the `position` that points to a usage of the clang symbol. - /// - isObjectiveCSelector: Whether the name is an Objective-C selector. Cannot be inferred from the name because - /// a name without `:` can also be a zero-arg Objective-C selector. For such names sourcekitd needs to know - /// whether it is translating a selector to apply the correct renaming rule. - /// - name: The clang symbol name. - /// - Returns: - fileprivate func translateClangNameToSwift( - at symbolLocation: SymbolLocation, - in snapshot: DocumentSnapshot, - isObjectiveCSelector: Bool, - name: String - ) async throws -> String { - let req = sourcekitd.dictionary([ - keys.sourceFile: snapshot.uri.pseudoPath, - keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs - as [SKDRequestValue]?, - keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)), - keys.nameKind: sourcekitd.values.nameObjc, - ]) - - if isObjectiveCSelector { - // Split the name into selector pieces, keeping the ':'. - let selectorPieces = name.split(separator: ":").map { String($0 + ":") } - req.set(keys.selectorPieces, to: sourcekitd.array(selectorPieces)) - } else { - req.set(keys.baseName, to: name) - } - - let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) - - guard let baseName: String = response[keys.baseName] else { - throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response) - } - let argNamesArray: SKDResponseArray? = response[keys.argNames] - let argNames = try argNamesArray?.map { (dict: SKDResponseDictionary) -> String in - guard var name: String = dict[keys.name] else { - throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response) - } - if name.isEmpty { - // Empty argument names are represented by `_` in Swift. - name = "_" - } - return name + ":" - } - var result = baseName - if let argNames, !argNames.isEmpty { - result += "(" + argNames.joined() + ")" - } - return result - } -} - /// A name that has a representation both in Swift and clang-based languages. /// /// These names might differ. For example, an Objective-C method gets translated by the clang importer to form the Swift /// name or it could have a `SWIFT_NAME` attribute that defines the method's name in Swift. Similarly, a Swift symbol /// might specify the name by which it gets exposed to Objective-C using the `@objc` attribute. package struct CrossLanguageName: Sendable { + package init(clangName: String? = nil, swiftName: String? = nil, definitionLanguage: Language) { + self.clangName = clangName + self.swiftName = swiftName + self.definitionLanguage = definitionLanguage + } + /// The name of the symbol in clang languages or `nil` if the symbol is defined in Swift, doesn't have any references /// from clang languages and thus hasn't been translated. package let clangName: String? @@ -458,13 +63,6 @@ package struct CrossLanguageName: Sendable { /// Swift and thus hasn't been translated. package let swiftName: String? - fileprivate var compoundSwiftName: CompoundDeclName? { - if let swiftName { - return CompoundDeclName(swiftName) - } - return nil - } - /// the language that the symbol is defined in. package let definitionLanguage: Language @@ -481,6 +79,21 @@ package struct CrossLanguageName: Sendable { } } +package protocol NameTranslatorService: Sendable { + func translateClangNameToSwift( + at symbolLocation: SymbolLocation, + in snapshot: DocumentSnapshot, + isObjectiveCSelector: Bool, + name: String + ) async throws -> String + + func translateSwiftNameToClang( + at symbolLocation: SymbolLocation, + in uri: DocumentURI, + name: String + ) async throws -> String +} + // MARK: - SourceKitLSPServer /// The kinds of symbol occurrence roles that should be renamed. @@ -493,7 +106,7 @@ extension SourceKitLSPServer { usr: String, index: CheckedIndex, workspace: Workspace - ) async -> (swiftLanguageService: SwiftLanguageService, snapshot: DocumentSnapshot, location: SymbolLocation)? { + ) async -> (swiftLanguageService: NameTranslatorService, snapshot: DocumentSnapshot, location: SymbolLocation)? { var reference: SymbolOccurrence? = nil index.forEachSymbolOccurrence(byUSR: usr, roles: renameRoles) { if $0.symbolProvider == .swift { @@ -511,7 +124,7 @@ extension SourceKitLSPServer { guard let snapshot = self.documentManager.latestSnapshotOrDisk(uri, language: .swift) else { return nil } - let swiftLanguageService = await self.languageService(for: uri, .swift, in: workspace) as? SwiftLanguageService + let swiftLanguageService = await self.languageService(for: uri, .swift, in: workspace) as? NameTranslatorService guard let swiftLanguageService else { return nil } @@ -601,7 +214,7 @@ extension SourceKitLSPServer { for: definitionDocumentUri, definitionLanguage, in: workspace - ) as? SwiftLanguageService + ) as? NameTranslatorService else { throw ResponseError.unknown("Failed to get language service for the document defining \(usr)") } @@ -616,7 +229,7 @@ extension SourceKitLSPServer { clangName = try await swiftLanguageService.translateSwiftNameToClang( at: definitionOccurrence.location, in: definitionDocumentUri, - name: CompoundDeclName(definitionName) + name: definitionName ) } else { clangName = nil @@ -852,526 +465,3 @@ extension SourceKitLSPServer { return try await languageService.indexedRename(request) } } - -// MARK: - Swift - -extension SwiftLanguageService { - /// From a list of rename locations compute the list of `SyntacticRenameName`s that define which ranges need to be - /// edited to rename a compound decl name. - /// - /// - Parameters: - /// - renameLocations: The locations to rename - /// - oldName: The compound decl name that the declaration had before the rename. Used to verify that the rename - /// locations match that name. Eg. `myFunc(argLabel:otherLabel:)` or `myVar` - /// - snapshot: A `DocumentSnapshot` containing the contents of the file for which to compute the rename ranges. - private func getSyntacticRenameRanges( - renameLocations: [RenameLocation], - oldName: String, - in snapshot: DocumentSnapshot - ) async throws -> [SyntacticRenameName] { - let locations = sourcekitd.array( - renameLocations.map { renameLocation in - let location = sourcekitd.dictionary([ - keys.line: renameLocation.line, - keys.column: renameLocation.utf8Column, - keys.nameType: renameLocation.usage.uid(values: values), - ]) - return sourcekitd.dictionary([ - keys.locations: [location], - keys.name: oldName, - ]) - } - ) - - let skreq = sourcekitd.dictionary([ - keys.sourceFile: snapshot.uri.pseudoPath, - // find-syntactic-rename-ranges is a syntactic sourcekitd request that doesn't use the in-memory file snapshot. - // We need to send the source text again. - keys.sourceText: snapshot.text, - keys.renameLocations: locations, - ]) - - let syntacticRenameRangesResponse = try await send(sourcekitdRequest: \.findRenameRanges, skreq, snapshot: snapshot) - guard let categorizedRanges: SKDResponseArray = syntacticRenameRangesResponse[keys.categorizedRanges] else { - throw ResponseError.internalError("sourcekitd did not return categorized ranges") - } - - return categorizedRanges.compactMap { SyntacticRenameName($0, in: snapshot, keys: keys, values: values) } - } - - /// If `position` is on an argument label or a parameter name, find the range from the function's base name to the - /// token that terminates the arguments or parameters of the function. Typically, this is the closing ')' but it can - /// also be a closing ']' for subscripts or the end of a trailing closure. - private func findFunctionLikeRange(of position: Position, in snapshot: DocumentSnapshot) async -> Range? { - let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot) - guard let token = tree.token(at: snapshot.absolutePosition(of: position)) else { - return nil - } - - // The node that contains the function's base name. This might be an expression like `self.doStuff`. - // The start position of the last token in this node will be used as the base name position. - var startToken: TokenSyntax? = nil - var endToken: TokenSyntax? = nil - - switch token.keyPathInParent { - case \LabeledExprSyntax.label: - let callLike = token.parent(as: LabeledExprSyntax.self)?.parent(as: LabeledExprListSyntax.self)?.parent - switch callLike?.as(SyntaxEnum.self) { - case .attribute(let attribute): - startToken = attribute.attributeName.lastToken(viewMode: .sourceAccurate) - endToken = attribute.lastToken(viewMode: .sourceAccurate) - case .functionCallExpr(let functionCall): - startToken = functionCall.calledExpression.lastToken(viewMode: .sourceAccurate) - endToken = functionCall.lastToken(viewMode: .sourceAccurate) - case .macroExpansionDecl(let macroExpansionDecl): - startToken = macroExpansionDecl.macroName - endToken = macroExpansionDecl.lastToken(viewMode: .sourceAccurate) - case .macroExpansionExpr(let macroExpansionExpr): - startToken = macroExpansionExpr.macroName - endToken = macroExpansionExpr.lastToken(viewMode: .sourceAccurate) - case .subscriptCallExpr(let subscriptCall): - startToken = subscriptCall.leftSquare - endToken = subscriptCall.lastToken(viewMode: .sourceAccurate) - default: - break - } - case \FunctionParameterSyntax.firstName: - let parameterClause = - token - .parent(as: FunctionParameterSyntax.self)? - .parent(as: FunctionParameterListSyntax.self)? - .parent(as: FunctionParameterClauseSyntax.self) - if let functionSignature = parameterClause?.parent(as: FunctionSignatureSyntax.self) { - switch functionSignature.parent?.as(SyntaxEnum.self) { - case .functionDecl(let functionDecl): - startToken = functionDecl.name - endToken = functionSignature.parameterClause.rightParen - case .initializerDecl(let initializerDecl): - startToken = initializerDecl.initKeyword - endToken = functionSignature.parameterClause.rightParen - case .macroDecl(let macroDecl): - startToken = macroDecl.name - endToken = functionSignature.parameterClause.rightParen - default: - break - } - } else if let subscriptDecl = parameterClause?.parent(as: SubscriptDeclSyntax.self) { - startToken = subscriptDecl.subscriptKeyword - endToken = subscriptDecl.parameterClause.rightParen - } - case \DeclNameArgumentSyntax.name: - let declReference = - token - .parent(as: DeclNameArgumentSyntax.self)? - .parent(as: DeclNameArgumentListSyntax.self)? - .parent(as: DeclNameArgumentsSyntax.self)? - .parent(as: DeclReferenceExprSyntax.self) - startToken = declReference?.baseName - endToken = declReference?.argumentNames?.rightParen - default: - break - } - - if let startToken, let endToken { - return snapshot.absolutePositionRange( - of: startToken.positionAfterSkippingLeadingTrivia.. (position: Position?, usr: String?, functionLikeRange: Range?) { - let startOfIdentifierPosition = await adjustPositionToStartOfIdentifier(position, in: snapshot) - let symbolInfo = try? await self.symbolInfo( - SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: startOfIdentifierPosition) - ) - - guard let functionLikeRange = await findFunctionLikeRange(of: startOfIdentifierPosition, in: snapshot) else { - return (startOfIdentifierPosition, symbolInfo?.only?.usr, nil) - } - if let onlySymbol = symbolInfo?.only, onlySymbol.kind == .constructor { - // We have a rename like `MyStruct(x: 1)`, invoked from `x`. - if let bestLocalDeclaration = onlySymbol.bestLocalDeclaration, bestLocalDeclaration.uri == snapshot.uri { - // If the initializer is declared within the same file, we can perform rename in the current file based on - // the declaration's location. - return (bestLocalDeclaration.range.lowerBound, onlySymbol.usr, functionLikeRange) - } - // Otherwise, we don't have a reference to the base name of the initializer and we can't use related - // identifiers to perform the rename. - // Return `nil` for the position to perform a pure index-based rename. - return (nil, onlySymbol.usr, functionLikeRange) - } - // Adjust the symbol info to the symbol info of the base name. - // This ensures that we get the symbol info of the function's base instead of the parameter. - let baseNameSymbolInfo = try? await self.symbolInfo( - SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: functionLikeRange.lowerBound) - ) - return (functionLikeRange.lowerBound, baseNameSymbolInfo?.only?.usr, functionLikeRange) - } - - package func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) { - let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri) - - let (renamePosition, usr, _) = await symbolToRename(at: request.position, in: snapshot) - guard let renamePosition else { - return (edits: WorkspaceEdit(), usr: usr) - } - - let relatedIdentifiersResponse = try await self.relatedIdentifiers( - at: renamePosition, - in: snapshot, - includeNonEditableBaseNames: true - ) - guard let oldNameString = relatedIdentifiersResponse.name else { - throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename") - } - - let renameLocations = relatedIdentifiersResponse.renameLocations(in: snapshot) - - try Task.checkCancellation() - - let oldName = CrossLanguageName(clangName: nil, swiftName: oldNameString, definitionLanguage: .swift) - let newName = CrossLanguageName(clangName: nil, swiftName: request.newName, definitionLanguage: .swift) - var edits = try await editsToRename( - locations: renameLocations, - in: snapshot, - oldName: oldName, - newName: newName - ) - if let compoundSwiftName = oldName.compoundSwiftName, !compoundSwiftName.parameters.isEmpty { - // If we are doing a function rename, run `renameParametersInFunctionBody` for every occurrence of the rename - // location within the current file. If the location is not a function declaration, it will exit early without - // invoking sourcekitd, so it's OK to do this performance-wise. - for renameLocation in renameLocations { - edits += await editsToRenameParametersInFunctionBody( - snapshot: snapshot, - renameLocation: renameLocation, - newName: newName - ) - } - } - edits = edits.filter { !$0.isNoOp(in: snapshot) } - - if edits.isEmpty { - return (edits: WorkspaceEdit(changes: [:]), usr: usr) - } - return (edits: WorkspaceEdit(changes: [snapshot.uri: edits]), usr: usr) - } - - package func editsToRenameParametersInFunctionBody( - snapshot: DocumentSnapshot, - renameLocation: RenameLocation, - newName: CrossLanguageName - ) async -> [TextEdit] { - let position = snapshot.absolutePosition(of: renameLocation) - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - let token = syntaxTree.token(at: position) - let parameterClause: FunctionParameterClauseSyntax? - switch token?.keyPathInParent { - case \FunctionDeclSyntax.name: - parameterClause = token?.parent(as: FunctionDeclSyntax.self)?.signature.parameterClause - case \InitializerDeclSyntax.initKeyword: - parameterClause = token?.parent(as: InitializerDeclSyntax.self)?.signature.parameterClause - case \SubscriptDeclSyntax.subscriptKeyword: - parameterClause = token?.parent(as: SubscriptDeclSyntax.self)?.parameterClause - default: - parameterClause = nil - } - guard let parameterClause else { - // We are not at a function-like definition. Nothing to rename. - return [] - } - guard let newSwiftNameString = newName.swiftName else { - logger.fault( - "Cannot rename at \(renameLocation.line):\(renameLocation.utf8Column) because new name is not a Swift name" - ) - return [] - } - let newSwiftName = CompoundDeclName(newSwiftNameString) - - var edits: [TextEdit] = [] - for (index, parameter) in parameterClause.parameters.enumerated() { - guard parameter.secondName == nil else { - // The parameter has a second name. The function signature only renames the first name and the function body - // refers to the second name. Nothing to do. - continue - } - let oldParameterName = parameter.firstName.text - guard index < newSwiftName.parameters.count else { - // We don't have a new name for this parameter. Nothing to do. - continue - } - let newParameterName = newSwiftName.parameters[index].stringOrEmpty - guard !newParameterName.isEmpty else { - // We are changing the parameter to an empty name. This will retain the current external parameter name as the - // new second name, so nothing to do in the function body. - continue - } - guard newParameterName != oldParameterName else { - // This parameter wasn't modified. Nothing to do. - continue - } - - let oldCrossLanguageParameterName = CrossLanguageName( - clangName: nil, - swiftName: oldParameterName, - definitionLanguage: .swift - ) - let newCrossLanguageParameterName = CrossLanguageName( - clangName: nil, - swiftName: newParameterName, - definitionLanguage: .swift - ) - - let parameterRenameEdits = await orLog("Renaming parameter") { - let parameterPosition = snapshot.position(of: parameter.positionAfterSkippingLeadingTrivia) - // Once we have lexical scope lookup in swift-syntax, this can be a purely syntactic rename. - // We know that the parameters are variables and thus there can't be overloads that need to be resolved by the - // type checker. - let relatedIdentifiers = try await self.relatedIdentifiers( - at: parameterPosition, - in: snapshot, - includeNonEditableBaseNames: false - ) - - // Exclude the edit that renames the parameter itself. The parameter gets renamed as part of the function - // declaration. - let filteredRelatedIdentifiers = RelatedIdentifiersResponse( - relatedIdentifiers: relatedIdentifiers.relatedIdentifiers.filter { !$0.range.contains(parameterPosition) }, - name: relatedIdentifiers.name - ) - - let parameterRenameLocations = filteredRelatedIdentifiers.renameLocations(in: snapshot) - - return try await editsToRename( - locations: parameterRenameLocations, - in: snapshot, - oldName: oldCrossLanguageParameterName, - newName: newCrossLanguageParameterName - ) - } - guard let parameterRenameEdits else { - continue - } - edits += parameterRenameEdits - } - return edits - } - - /// Return the edit that needs to be performed for the given syntactic rename piece to rename it from - /// `oldParameter` to `newParameter`. - /// Returns `nil` if no edit needs to be performed. - private func textEdit( - for piece: SyntacticRenamePiece, - in snapshot: DocumentSnapshot, - oldParameter: CompoundDeclName.Parameter, - newParameter: CompoundDeclName.Parameter - ) -> TextEdit? { - switch piece.kind { - case .parameterName: - if newParameter == .wildcard, piece.range.isEmpty, case .named(let oldParameterName) = oldParameter { - // We are changing a named parameter to an unnamed one. If the parameter didn't have an internal parameter - // name, we need to transfer the previously external parameter name to be the internal one. - // E.g. `func foo(a: Int)` becomes `func foo(_ a: Int)`. - return TextEdit(range: piece.range, newText: " " + oldParameterName) - } - if case .named(let newParameterLabel) = newParameter, - newParameterLabel.trimmingCharacters(in: .whitespaces) - == snapshot.lineTable[piece.range].trimmingCharacters(in: .whitespaces) - { - // We are changing the external parameter name to be the same one as the internal parameter name. The - // internal name is thus no longer needed. Drop it. - // Eg. an old declaration `func foo(_ a: Int)` becomes `func foo(a: Int)` when renaming the parameter to `a` - return TextEdit(range: piece.range, newText: "") - } - // In all other cases, don't touch the internal parameter name. It's not part of the public API. - return nil - case .noncollapsibleParameterName: - // Noncollapsible parameter names should never be renamed because they are the same as `parameterName` but - // never fall into one of the two categories above. - return nil - case .declArgumentLabel: - if piece.range.isEmpty { - // If we are inserting a new external argument label where there wasn't one before, add a space after it to - // separate it from the internal name. - // E.g. `subscript(a: Int)` becomes `subscript(a a: Int)`. - return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard + " ") - } - // Otherwise, just update the name. - return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard) - case .callArgumentLabel: - // Argument labels of calls are just updated. - return TextEdit(range: piece.range, newText: newParameter.stringOrEmpty) - case .callArgumentColon: - if case .wildcard = newParameter { - // If the parameter becomes unnamed, remove the colon after the argument name. - return TextEdit(range: piece.range, newText: "") - } - return nil - case .callArgumentCombined: - if case .named(let newParameterName) = newParameter { - // If an unnamed parameter becomes named, insert the new name and a colon. - return TextEdit(range: piece.range, newText: newParameterName + ": ") - } - return nil - case .selectorArgumentLabel: - return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard) - case .baseName, .keywordBaseName: - preconditionFailure("Handled above") - } - } - - package func editsToRename( - locations renameLocations: [RenameLocation], - in snapshot: DocumentSnapshot, - oldName oldCrossLanguageName: CrossLanguageName, - newName newCrossLanguageName: CrossLanguageName - ) async throws -> [TextEdit] { - guard - let oldNameString = oldCrossLanguageName.swiftName, - let oldName = oldCrossLanguageName.compoundSwiftName, - let newName = newCrossLanguageName.compoundSwiftName - else { - throw ResponseError.unknown( - "Failed to rename \(snapshot.uri.forLogging) because the Swift name for rename is unknown" - ) - } - - let tree = await syntaxTreeManager.syntaxTree(for: snapshot) - - let compoundRenameRanges = try await getSyntacticRenameRanges( - renameLocations: renameLocations, - oldName: oldNameString, - in: snapshot - ) - - try Task.checkCancellation() - - return compoundRenameRanges.flatMap { (compoundRenameRange) -> [TextEdit] in - switch compoundRenameRange.category { - case .unmatched, .mismatch: - // The location didn't match. Don't rename it - return [] - case .activeCode, .inactiveCode, .selector: - // Occurrences in active code and selectors should always be renamed. - // Inactive code is currently never returned by sourcekitd. - break - case .string, .comment: - // We currently never get any results in strings or comments because the related identifiers request doesn't - // provide any locations inside strings or comments. We would need to have a textual index to find these - // locations. - return [] - } - return compoundRenameRange.pieces.compactMap { (piece) -> TextEdit? in - if piece.kind == .baseName { - if let firstNameToken = tree.token(at: snapshot.absolutePosition(of: piece.range.lowerBound)), - firstNameToken.keyPathInParent == \FunctionParameterSyntax.firstName, - let parameterSyntax = firstNameToken.parent(as: FunctionParameterSyntax.self), - parameterSyntax.secondName == nil // Should always be true because otherwise decl would be second name - { - // We are renaming a function parameter from inside the function body. - // This should be a local rename and it shouldn't affect all the callers of the function. Introduce the new - // name as a second name. - return TextEdit( - range: Range(snapshot.position(of: firstNameToken.endPositionBeforeTrailingTrivia)), - newText: " " + newName.baseName - ) - } - - return TextEdit(range: piece.range, newText: newName.baseName) - } else if piece.kind == .keywordBaseName { - // Keyword base names can't be renamed - return nil - } - - guard let parameterIndex = piece.parameterIndex, - parameterIndex < newName.parameters.count, - parameterIndex < oldName.parameters.count - else { - // Be lenient and just keep the old parameter names if the new name doesn't specify them, eg. if we are - // renaming `func foo(a: Int, b: Int)` and the user specified `bar(x:)` as the new name. - return nil - } - - return self.textEdit( - for: piece, - in: snapshot, - oldParameter: oldName.parameters[parameterIndex], - newParameter: newName.parameters[parameterIndex] - ) - } - } - } - - package func prepareRename( - _ request: PrepareRenameRequest - ) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? { - let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri) - - let (renamePosition, usr, functionLikeRange) = await symbolToRename(at: request.position, in: snapshot) - guard let renamePosition else { - return nil - } - - let response = try await self.relatedIdentifiers( - at: renamePosition, - in: snapshot, - includeNonEditableBaseNames: true - ) - guard var name = response.name else { - throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename") - } - if name.hasSuffix("()") { - name = String(name.dropLast(2)) - } - guard let relatedIdentRange = response.relatedIdentifiers.first(where: { $0.range.contains(renamePosition) })?.range - else { - return nil - } - return (PrepareRenameResponse(range: functionLikeRange ?? relatedIdentRange, placeholder: name), usr) - } -} - -// MARK: - Clang - -fileprivate extension SyntaxProtocol { - /// Returns the parent node and casts it to the specified type. - func parent(as syntaxType: S.Type) -> S? { - return parent?.as(S.self) - } -} - -fileprivate extension RelatedIdentifiersResponse { - func renameLocations(in snapshot: DocumentSnapshot) -> [RenameLocation] { - return self.relatedIdentifiers.map { - (relatedIdentifier) -> RenameLocation in - let position = relatedIdentifier.range.lowerBound - let utf8Column = snapshot.lineTable.utf8ColumnAt(line: position.line, utf16Column: position.utf16index) - return RenameLocation(line: position.line + 1, utf8Column: utf8Column + 1, usage: relatedIdentifier.usage) - } - } -} diff --git a/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift b/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift index 2c586fc1d..7982badae 100644 --- a/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift +++ b/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift @@ -46,7 +46,7 @@ extension WorkDoneProgressManager { /// running, it displays a work done progress in the client. If multiple operations are running at the same time, it /// doesn't show multiple work done progress in the client. For example, we only want to show one progress indicator /// when sourcekitd has crashed, not one per `SwiftLanguageService`. -actor SharedWorkDoneProgressManager { +package actor SharedWorkDoneProgressManager { private weak var sourceKitLSPServer: SourceKitLSPServer? /// The number of in-progress operations. When greater than 0 `workDoneProgress` is non-nil and a work done progress @@ -58,7 +58,7 @@ actor SharedWorkDoneProgressManager { private let title: String private let message: String? - package init( + init( sourceKitLSPServer: SourceKitLSPServer, tokenPrefix: String, title: String, @@ -70,7 +70,7 @@ actor SharedWorkDoneProgressManager { self.message = message } - func start() async { + package func start() async { guard let sourceKitLSPServer else { return } @@ -92,7 +92,7 @@ actor SharedWorkDoneProgressManager { } } - func end() async { + package func end() async { if inProgressOperations > 0 { inProgressOperations -= 1 } else { diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index c11ce4d15..8bc1c4fb7 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -35,14 +35,6 @@ import DocCDocumentation /// Disambiguate LanguageServerProtocol.Language and IndexstoreDB.Language package typealias Language = LanguageServerProtocol.Language -struct LanguageServiceIdentifier: Hashable { - private let identifier: String - - init(type: LanguageService.Type) { - self.identifier = String(reflecting: type) - } -} - /// The SourceKit-LSP server. /// /// This is the client-facing language server implementation, providing indexing, multiple-toolchain @@ -65,7 +57,7 @@ package actor SourceKitLSPServer { private let workspaceQueue = AsyncQueue() /// The connection to the editor. - package let client: Connection + package nonisolated let client: Connection /// Set to `true` after the `SourceKitLSPServer` has send the reply to the `InitializeRequest`. /// @@ -73,7 +65,7 @@ package actor SourceKitLSPServer { private var initialized: Bool = false private let _options: ThreadSafeBox - nonisolated var options: SourceKitLSPOptions { + nonisolated package var options: SourceKitLSPOptions { _options.value } @@ -83,11 +75,11 @@ package actor SourceKitLSPServer { package var capabilityRegistry: CapabilityRegistry? - private let languageServiceRegistry: LanguageServiceRegistry + let languageServiceRegistry: LanguageServiceRegistry - var languageServices: [LanguageServiceIdentifier: [LanguageService]] = [:] + var languageServices: [LanguageServiceType: [LanguageService]] = [:] - package let documentManager = DocumentManager() + package nonisolated let documentManager = DocumentManager() /// The `TaskScheduler` that schedules all background indexing tasks. /// @@ -105,7 +97,7 @@ package actor SourceKitLSPServer { /// `SourceKitLSPServer`. /// `nonisolated(unsafe)` because `sourcekitdCrashedWorkDoneProgress` will not be modified after it is assigned from /// the initializer. - nonisolated(unsafe) var sourcekitdCrashedWorkDoneProgress: SharedWorkDoneProgressManager! + nonisolated(unsafe) package private(set) var sourcekitdCrashedWorkDoneProgress: SharedWorkDoneProgressManager! /// Stores which workspace the given URI has been opened in. /// @@ -466,7 +458,7 @@ package actor SourceKitLSPServer { toolchain: Toolchain, workspace: Workspace ) -> LanguageService? { - for languageService in languageServices[LanguageServiceIdentifier(type: serverType), default: []] { + for languageService in languageServices[LanguageServiceType(serverType), default: []] { if languageService.canHandle(workspace: workspace, toolchain: toolchain) { return languageService } @@ -550,7 +542,7 @@ package actor SourceKitLSPServer { return concurrentlyInitializedService } - languageServices[LanguageServiceIdentifier(type: serverType), default: []].append(service) + languageServices[LanguageServiceType(serverType), default: []].append(service) return service } } @@ -1085,7 +1077,7 @@ extension SourceKitLSPServer { let executeCommandOptions = await registry.clientHasDynamicExecuteCommandRegistration ? nil - : ExecuteCommandOptions(commands: builtinSwiftCommands) + : ExecuteCommandOptions(commands: languageServiceRegistry.languageServices.flatMap { $0.type.builtInCommands }) var experimentalCapabilities: [String: LSPAny] = [ WorkspaceTestsRequest.method: .dictionary(["version": .int(2)]), @@ -1572,10 +1564,13 @@ extension SourceKitLSPServer { func completionItemResolve( request: CompletionItemResolveRequest ) async throws -> CompletionItem { - guard let completionItemData = CompletionItemData(fromLSPAny: request.item.data) else { + // Swift completion items specify the URI of the item they originate from in the `data` + guard case .dictionary(let dict) = request.item.data, case .string(let uriString) = dict["uri"], + let uri = try? DocumentURI(string: uriString) + else { return request.item } - return try await documentService(for: completionItemData.uri).completionItemResolve(request) + return try await documentService(for: uri).completionItemResolve(request) } #if canImport(DocCDocumentation) diff --git a/Sources/SourceKitLSP/SymbolLocation+DocumentURI.swift b/Sources/SourceKitLSP/SymbolLocation+DocumentURI.swift index 0496d0744..44427a462 100644 --- a/Sources/SourceKitLSP/SymbolLocation+DocumentURI.swift +++ b/Sources/SourceKitLSP/SymbolLocation+DocumentURI.swift @@ -11,10 +11,10 @@ //===----------------------------------------------------------------------===// import IndexStoreDB -import LanguageServerProtocol +package import LanguageServerProtocol extension SymbolLocation { - var documentUri: DocumentURI { + package var documentUri: DocumentURI { return DocumentURI(filePath: self.path, isDirectory: false) } } diff --git a/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift b/Sources/SourceKitLSP/SyntacticTestIndex.swift similarity index 90% rename from Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift rename to Sources/SourceKitLSP/SyntacticTestIndex.swift index ebf20091b..b2701dd82 100644 --- a/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift +++ b/Sources/SourceKitLSP/SyntacticTestIndex.swift @@ -73,36 +73,13 @@ private struct IndexedTests { let sourceFileModificationDate: Date } -/// Syntactically scans the file at the given URL for tests declared within it. -/// -/// Does not write the results to the index. -/// -/// The order of the returned tests is not defined. The results should be sorted before being returned to the editor. -private func testItems(in url: URL) async -> [AnnotatedTestItem] { - guard url.pathExtension == "swift" else { - return [] - } - let syntaxTreeManager = SyntaxTreeManager() - let snapshot = orLog("Getting document snapshot for swift-testing scanning") { - try DocumentSnapshot(withContentsFromDisk: url, language: .swift) - } - guard let snapshot else { - return [] - } - async let swiftTestingTests = SyntacticSwiftTestingTestScanner.findTestSymbols( - in: snapshot, - syntaxTreeManager: syntaxTreeManager - ) - async let xcTests = SyntacticSwiftXCTestScanner.findTestSymbols(in: snapshot, syntaxTreeManager: syntaxTreeManager) - - return await swiftTestingTests + xcTests -} - /// An in-memory syntactic index of test items within a workspace. /// /// The index does not get persisted to disk but instead gets rebuilt every time a workspace is opened (ie. usually when /// sourcekit-lsp is launched). Building it takes only a few seconds, even for large projects. actor SyntacticTestIndex { + private let languageServiceRegistry: LanguageServiceRegistry + /// The tests discovered by the index. private var indexedTests: [DocumentURI: IndexedTests] = [:] @@ -120,7 +97,11 @@ actor SyntacticTestIndex { /// indexing tasks to finish. private let indexingQueue = AsyncQueue() - init(determineTestFiles: @Sendable @escaping () async -> [DocumentURI]) { + init( + languageServiceRegistry: LanguageServiceRegistry, + determineTestFiles: @Sendable @escaping () async -> [DocumentURI] + ) { + self.languageServiceRegistry = languageServiceRegistry indexingQueue.async(priority: .low, metadata: .initialPopulation) { let testFiles = await determineTestFiles() @@ -228,7 +209,7 @@ actor SyntacticTestIndex { /// - Important: This method must be called in a task that is executing on `indexingQueue`. private func rescanFileAssumingOnQueue(_ uri: DocumentURI) async { guard let url = uri.fileURL else { - logger.log("Not indexing \(uri.forLogging) for swift-testing tests because it is not a file URL") + logger.log("Not indexing \(uri.forLogging) for tests because it is not a file URL") return } if Task.isCancelled { @@ -258,7 +239,13 @@ actor SyntacticTestIndex { if Task.isCancelled { return } - let testItems = await testItems(in: url) + guard let language = Language(inferredFromFileExtension: uri), + let languageServiceType = languageServiceRegistry.languageService(for: language) + else { + logger.log("Not indexing \(uri.forLogging) because the language service could not be inferred") + return + } + let testItems = await languageServiceType.syntacticTestItems(in: uri) guard !removedFiles.contains(uri) else { // Check whether the file got removed while we were scanning it for tests. If so, don't add it back to diff --git a/Sources/SourceKitLSP/TestDiscovery.swift b/Sources/SourceKitLSP/TestDiscovery.swift index 6fc44fa4b..9afdfb224 100644 --- a/Sources/SourceKitLSP/TestDiscovery.swift +++ b/Sources/SourceKitLSP/TestDiscovery.swift @@ -11,14 +11,12 @@ //===----------------------------------------------------------------------===// import BuildServerIntegration -import BuildServerProtocol import Foundation -import IndexStoreDB -package import LanguageServerProtocol +package import IndexStoreDB +import LanguageServerProtocol import SKLogging import SemanticIndex import SwiftExtensions -import SwiftSyntax package enum TestStyle { package static let xcTest = "XCTest" @@ -404,7 +402,7 @@ extension AnnotatedTestItem { /// Use out-of-date semantic information to filter syntactic symbols. /// /// Delegates to the `TestItem`'s `filterUsing(semanticSymbols:)` method to perform the filtering. - fileprivate func filterUsing(semanticSymbols: [Symbol]?) -> AnnotatedTestItem? { + package func filterUsing(semanticSymbols: [Symbol]?) -> AnnotatedTestItem? { guard let testItem = self.testItem.filterUsing(semanticSymbols: semanticSymbols) else { return nil } @@ -594,32 +592,3 @@ extension TestItem { return newTest } } - -extension SwiftLanguageService { - package func syntacticDocumentTests( - for uri: DocumentURI, - in workspace: Workspace - ) async throws -> [AnnotatedTestItem]? { - let targetIdentifiers = await workspace.buildServerManager.targets(for: uri) - let isInTestTarget = await targetIdentifiers.asyncContains(where: { - await workspace.buildServerManager.buildTarget(named: $0)?.tags.contains(.test) ?? true - }) - if !targetIdentifiers.isEmpty && !isInTestTarget { - // If we know the targets for the file and the file is not part of any test target, don't scan it for tests. - return nil - } - let snapshot = try documentManager.latestSnapshot(uri) - let semanticSymbols = workspace.index(checkedFor: .deletedFiles)?.symbols(inFilePath: snapshot.uri.pseudoPath) - let xctestSymbols = await SyntacticSwiftXCTestScanner.findTestSymbols( - in: snapshot, - syntaxTreeManager: syntaxTreeManager - ) - .compactMap { $0.filterUsing(semanticSymbols: semanticSymbols) } - - let swiftTestingSymbols = await SyntacticSwiftTestingTestScanner.findTestSymbols( - in: snapshot, - syntaxTreeManager: syntaxTreeManager - ) - return (xctestSymbols + swiftTestingSymbols).sorted { $0.testItem.location < $1.testItem.location } - } -} diff --git a/Sources/SourceKitLSP/TextEdit+IsNoop.swift b/Sources/SourceKitLSP/TextEdit+IsNoop.swift index dc4d3a59c..b5ac0ce4e 100644 --- a/Sources/SourceKitLSP/TextEdit+IsNoop.swift +++ b/Sources/SourceKitLSP/TextEdit+IsNoop.swift @@ -14,7 +14,7 @@ import LanguageServerProtocol extension TextEdit { /// Returns `true` the replaced text is the same as the new text - func isNoOp(in snapshot: DocumentSnapshot) -> Bool { + package func isNoOp(in snapshot: DocumentSnapshot) -> Bool { if snapshot.text[snapshot.indexRange(of: range)] == newText { return true } diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index b1f8ab1ea..41fd7dc05 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -128,7 +128,7 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { /// indexing and preparation tasks for files with out-of-date index. /// /// `nil` if background indexing is not enabled. - let semanticIndexManager: SemanticIndexManager? + package let semanticIndexManager: SemanticIndexManager? /// If the index uses explicit output paths, the queue on which we update the explicit output paths. /// @@ -137,7 +137,7 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { private let indexUnitOutputPathsUpdateQueue = AsyncQueue() private init( - sourceKitLSPServer: SourceKitLSPServer?, + sourceKitLSPServer: SourceKitLSPServer, rootUri: DocumentURI?, capabilityRegistry: CapabilityRegistry, options: SourceKitLSPOptions, @@ -182,11 +182,14 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { self.semanticIndexManager = nil } // Trigger an initial population of `syntacticTestIndex`. - self.syntacticTestIndex = SyntacticTestIndex(determineTestFiles: { - await orLog("Getting list of test files for initial syntactic index population") { - try await buildServerManager.testFiles() - } ?? [] - }) + self.syntacticTestIndex = SyntacticTestIndex( + languageServiceRegistry: sourceKitLSPServer.languageServiceRegistry, + determineTestFiles: { + await orLog("Getting list of test files for initial syntactic index population") { + try await buildServerManager.testFiles() + } ?? [] + } + ) await indexDelegate?.addMainFileChangedCallback { [weak self] in await self?.buildServerManager.mainFilesChanged() } @@ -369,12 +372,13 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { package static func forTesting( options: SourceKitLSPOptions, + sourceKitLSPServer: SourceKitLSPServer, testHooks: Hooks, buildServerManager: BuildServerManager, indexTaskScheduler: TaskScheduler ) async -> Workspace { return await Workspace( - sourceKitLSPServer: nil, + sourceKitLSPServer: sourceKitLSPServer, rootUri: nil, capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()), options: options, @@ -388,7 +392,7 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { /// Returns a `CheckedIndex` that verifies that all the returned entries are up-to-date with the given /// `IndexCheckLevel`. - func index(checkedFor checkLevel: IndexCheckLevel) -> CheckedIndex? { + package func index(checkedFor checkLevel: IndexCheckLevel) -> CheckedIndex? { return _uncheckedIndex.value?.checked(for: checkLevel) } diff --git a/Sources/SourceKitLSP/Swift/AdjustPositionToStartOfIdentifier.swift b/Sources/SwiftLanguageService/AdjustPositionToStartOfIdentifier.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/AdjustPositionToStartOfIdentifier.swift rename to Sources/SwiftLanguageService/AdjustPositionToStartOfIdentifier.swift index 7acf20302..3ffb08311 100644 --- a/Sources/SourceKitLSP/Swift/AdjustPositionToStartOfIdentifier.swift +++ b/Sources/SwiftLanguageService/AdjustPositionToStartOfIdentifier.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import LanguageServerProtocol +import SourceKitLSP import SwiftSyntax private class StartOfIdentifierFinder: SyntaxAnyVisitor { diff --git a/Sources/SwiftLanguageService/CMakeLists.txt b/Sources/SwiftLanguageService/CMakeLists.txt new file mode 100644 index 000000000..1e4a3ef97 --- /dev/null +++ b/Sources/SwiftLanguageService/CMakeLists.txt @@ -0,0 +1,86 @@ +add_library(SwiftLanguageService STATIC + SemanticRefactoring.swift + DoccDocumentation.swift + SwiftTestingScanner.swift + FoldingRange.swift + CMakeLists.txt + SymbolInfo.swift + RefactoringEdit.swift + CodeActions/ConvertStringConcatenationToStringInterpolation.swift + CodeActions/SyntaxRefactoringCodeActionProvider.swift + CodeActions/ConvertJSONToCodableStruct.swift + CodeActions/SyntaxCodeActionProvider.swift + CodeActions/SyntaxCodeActions.swift + CodeActions/ConvertIntegerLiteral.swift + CodeActions/PackageManifestEdits.swift + CodeActions/AddDocumentation.swift + RefactoringResponse.swift + SyntacticSwiftXCTestScanner.swift + CommentXML.swift + SymbolGraph.swift + ClosureCompletionFormat.swift + SemanticRefactorCommand.swift + DocumentFormatting.swift + DiagnosticReportManager.swift + WithSnapshotFromDiskOpenedInSourcekitd.swift + SemanticTokens.swift + ExpandMacroCommand.swift + Diagnostic.swift + CodeCompletion.swift + SwiftCodeLensScanner.swift + SwiftCommand.swift + RelatedIdentifiers.swift + VariableTypeInfo.swift + CodeCompletionSession.swift + SyntaxTreeManager.swift + Rename.swift + RewriteSourceKitPlaceholders.swift + SwiftLanguageService.swift + TestDiscovery.swift + OpenInterface.swift + SyntaxHighlightingToken.swift + MacroExpansion.swift + AdjustPositionToStartOfIdentifier.swift + SyntaxHighlightingTokenParser.swift + CursorInfo.swift + DocumentSymbols.swift + SyntaxHighlightingTokens.swift + GeneratedInterfaceManager.swift +) +set_target_properties(SwiftLanguageService PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) +target_link_libraries(SwiftLanguageService PUBLIC + BuildServerIntegration + LanguageServerProtocol + SKOptions + SourceKitD + SourceKitLSP + ToolchainRegistry + IndexStoreDB + SwiftSyntax::SwiftBasicFormat + SwiftSyntax::SwiftSyntax +) + +target_link_libraries(SourceKitLSP PRIVATE + BuildServerProtocol + Csourcekitd + LanguageServerProtocolExtensions + LanguageServerProtocolJSONRPC + SemanticIndex + SKLogging + SKUtilities + SwiftExtensions + TSCExtensions + Crypto + TSCBasic + PackageModel + PackageModelSyntax + SwiftSyntax::SwiftDiagnostics + SwiftSyntax::SwiftIDEUtils + SwiftSyntax::SwiftParser + SwiftSyntax::SwiftParserDiagnostics + SwiftSyntax::SwiftRefactor + SwiftSyntax::SwiftSyntaxBuilder + $<$>:FoundationXML> +) + diff --git a/Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift b/Sources/SwiftLanguageService/ClosureCompletionFormat.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/ClosureCompletionFormat.swift rename to Sources/SwiftLanguageService/ClosureCompletionFormat.swift diff --git a/Sources/SourceKitLSP/Swift/CodeActions/AddDocumentation.swift b/Sources/SwiftLanguageService/CodeActions/AddDocumentation.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/CodeActions/AddDocumentation.swift rename to Sources/SwiftLanguageService/CodeActions/AddDocumentation.swift diff --git a/Sources/SourceKitLSP/Swift/CodeActions/ConvertIntegerLiteral.swift b/Sources/SwiftLanguageService/CodeActions/ConvertIntegerLiteral.swift similarity index 98% rename from Sources/SourceKitLSP/Swift/CodeActions/ConvertIntegerLiteral.swift rename to Sources/SwiftLanguageService/CodeActions/ConvertIntegerLiteral.swift index b8f8e9505..bb84a2d1f 100644 --- a/Sources/SourceKitLSP/Swift/CodeActions/ConvertIntegerLiteral.swift +++ b/Sources/SwiftLanguageService/CodeActions/ConvertIntegerLiteral.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import LanguageServerProtocol +import SourceKitLSP import SwiftRefactor import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Sources/SourceKitLSP/Swift/CodeActions/ConvertJSONToCodableStruct.swift b/Sources/SwiftLanguageService/CodeActions/ConvertJSONToCodableStruct.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/CodeActions/ConvertJSONToCodableStruct.swift rename to Sources/SwiftLanguageService/CodeActions/ConvertJSONToCodableStruct.swift diff --git a/Sources/SourceKitLSP/Swift/CodeActions/ConvertStringConcatenationToStringInterpolation.swift b/Sources/SwiftLanguageService/CodeActions/ConvertStringConcatenationToStringInterpolation.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/CodeActions/ConvertStringConcatenationToStringInterpolation.swift rename to Sources/SwiftLanguageService/CodeActions/ConvertStringConcatenationToStringInterpolation.swift diff --git a/Sources/SourceKitLSP/Swift/CodeActions/PackageManifestEdits.swift b/Sources/SwiftLanguageService/CodeActions/PackageManifestEdits.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/CodeActions/PackageManifestEdits.swift rename to Sources/SwiftLanguageService/CodeActions/PackageManifestEdits.swift index d2be13558..34a7d1077 100644 --- a/Sources/SourceKitLSP/Swift/CodeActions/PackageManifestEdits.swift +++ b/Sources/SwiftLanguageService/CodeActions/PackageManifestEdits.swift @@ -12,6 +12,7 @@ import Foundation import LanguageServerProtocol +import SourceKitLSP import SwiftParser @_spi(PackageRefactor) import SwiftRefactor import SwiftSyntax diff --git a/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActionProvider.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActionProvider.swift rename to Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift index f37be8df1..d3ccc085e 100644 --- a/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActionProvider.swift +++ b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift @@ -12,6 +12,7 @@ import LanguageServerProtocol import SKLogging +import SourceKitLSP import SwiftRefactor import SwiftSyntax diff --git a/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActions.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActions.swift rename to Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift diff --git a/Sources/SourceKitLSP/Swift/CodeActions/SyntaxRefactoringCodeActionProvider.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxRefactoringCodeActionProvider.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/CodeActions/SyntaxRefactoringCodeActionProvider.swift rename to Sources/SwiftLanguageService/CodeActions/SyntaxRefactoringCodeActionProvider.swift index 6e5890c8a..a35afa8f7 100644 --- a/Sources/SourceKitLSP/Swift/CodeActions/SyntaxRefactoringCodeActionProvider.swift +++ b/Sources/SwiftLanguageService/CodeActions/SyntaxRefactoringCodeActionProvider.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import LanguageServerProtocol +import SourceKitLSP import SwiftRefactor import SwiftSyntax diff --git a/Sources/SourceKitLSP/Swift/CodeCompletion.swift b/Sources/SwiftLanguageService/CodeCompletion.swift similarity index 98% rename from Sources/SourceKitLSP/Swift/CodeCompletion.swift rename to Sources/SwiftLanguageService/CodeCompletion.swift index cd3471c40..69f1e1e52 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletion.swift +++ b/Sources/SwiftLanguageService/CodeCompletion.swift @@ -14,6 +14,7 @@ import Foundation package import LanguageServerProtocol import SKLogging import SourceKitD +import SourceKitLSP import SwiftBasicFormat extension SwiftLanguageService { diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SwiftLanguageService/CodeCompletionSession.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/CodeCompletionSession.swift rename to Sources/SwiftLanguageService/CodeCompletionSession.swift index 5e4f2e1da..3f0a85406 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SwiftLanguageService/CodeCompletionSession.swift @@ -18,6 +18,7 @@ import SKLogging import SKOptions import SKUtilities import SourceKitD +import SourceKitLSP import SwiftExtensions import SwiftParser @_spi(SourceKitLSP) import SwiftRefactor diff --git a/Sources/SourceKitLSP/Swift/CommentXML.swift b/Sources/SwiftLanguageService/CommentXML.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/CommentXML.swift rename to Sources/SwiftLanguageService/CommentXML.swift diff --git a/Sources/SourceKitLSP/Swift/CursorInfo.swift b/Sources/SwiftLanguageService/CursorInfo.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/CursorInfo.swift rename to Sources/SwiftLanguageService/CursorInfo.swift index e82ccf295..3e643d62f 100644 --- a/Sources/SourceKitLSP/Swift/CursorInfo.swift +++ b/Sources/SwiftLanguageService/CursorInfo.swift @@ -14,6 +14,7 @@ import Csourcekitd import LanguageServerProtocol import SKLogging import SourceKitD +import SourceKitLSP /// Detailed information about a symbol under the cursor. /// diff --git a/Sources/SourceKitLSP/Swift/Diagnostic.swift b/Sources/SwiftLanguageService/Diagnostic.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/Diagnostic.swift rename to Sources/SwiftLanguageService/Diagnostic.swift index 4512c3c3b..6a844432b 100644 --- a/Sources/SourceKitLSP/Swift/Diagnostic.swift +++ b/Sources/SwiftLanguageService/Diagnostic.swift @@ -16,10 +16,13 @@ import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging import SourceKitD +import SourceKitLSP import SwiftDiagnostics import SwiftExtensions import SwiftSyntax +import struct SourceKitLSP.Diagnostic + extension CodeAction { /// Creates a CodeAction from a list for sourcekit fixits. /// diff --git a/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift b/Sources/SwiftLanguageService/DiagnosticReportManager.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift rename to Sources/SwiftLanguageService/DiagnosticReportManager.swift index 8258010d0..3c44179b1 100644 --- a/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift +++ b/Sources/SwiftLanguageService/DiagnosticReportManager.swift @@ -16,10 +16,13 @@ import SKLogging import SKOptions import SKUtilities import SourceKitD +import SourceKitLSP import SwiftDiagnostics import SwiftExtensions import SwiftParserDiagnostics +import struct SourceKitLSP.Diagnostic + actor DiagnosticReportManager { /// A task to produce diagnostics, either from a diagnostics request to `sourcekitd` or by using the built-in swift-syntax. private typealias ReportTask = RefCountedCancellableTask< diff --git a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift b/Sources/SwiftLanguageService/DoccDocumentation.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/DoccDocumentation.swift rename to Sources/SwiftLanguageService/DoccDocumentation.swift index 21857e919..f050017f1 100644 --- a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift +++ b/Sources/SwiftLanguageService/DoccDocumentation.swift @@ -20,6 +20,7 @@ import SemanticIndex import SKLogging import SwiftExtensions import SwiftSyntax +import SourceKitLSP extension SwiftLanguageService { package func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse { diff --git a/Sources/SourceKitLSP/Swift/DocumentFormatting.swift b/Sources/SwiftLanguageService/DocumentFormatting.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/DocumentFormatting.swift rename to Sources/SwiftLanguageService/DocumentFormatting.swift index 0a5d0e38d..635dd8935 100644 --- a/Sources/SourceKitLSP/Swift/DocumentFormatting.swift +++ b/Sources/SwiftLanguageService/DocumentFormatting.swift @@ -16,6 +16,7 @@ import LanguageServerProtocolExtensions import LanguageServerProtocolJSONRPC import SKLogging import SKUtilities +import SourceKitLSP import SwiftExtensions import SwiftParser import SwiftSyntax diff --git a/Sources/SourceKitLSP/Swift/DocumentSymbols.swift b/Sources/SwiftLanguageService/DocumentSymbols.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/DocumentSymbols.swift rename to Sources/SwiftLanguageService/DocumentSymbols.swift index 6b8c25e57..0a732a041 100644 --- a/Sources/SourceKitLSP/Swift/DocumentSymbols.swift +++ b/Sources/SwiftLanguageService/DocumentSymbols.swift @@ -13,6 +13,7 @@ import Foundation package import LanguageServerProtocol import SKLogging +import SourceKitLSP import SwiftSyntax extension SwiftLanguageService { diff --git a/Sources/SourceKitLSP/Swift/ExpandMacroCommand.swift b/Sources/SwiftLanguageService/ExpandMacroCommand.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/ExpandMacroCommand.swift rename to Sources/SwiftLanguageService/ExpandMacroCommand.swift diff --git a/Sources/SourceKitLSP/Swift/FoldingRange.swift b/Sources/SwiftLanguageService/FoldingRange.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/FoldingRange.swift rename to Sources/SwiftLanguageService/FoldingRange.swift index 6801934d9..0b04f8836 100644 --- a/Sources/SourceKitLSP/Swift/FoldingRange.swift +++ b/Sources/SwiftLanguageService/FoldingRange.swift @@ -13,6 +13,7 @@ package import LanguageServerProtocol import SKLogging import SKUtilities +import SourceKitLSP import SwiftSyntax private final class FoldingRangeFinder: SyntaxAnyVisitor { diff --git a/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift b/Sources/SwiftLanguageService/GeneratedInterfaceManager.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift rename to Sources/SwiftLanguageService/GeneratedInterfaceManager.swift index 3164a758a..bedb73b67 100644 --- a/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift +++ b/Sources/SwiftLanguageService/GeneratedInterfaceManager.swift @@ -14,6 +14,7 @@ import LanguageServerProtocol import SKLogging import SKUtilities import SourceKitD +import SourceKitLSP import SwiftExtensions /// When information about a generated interface is requested, this opens the generated interface in sourcekitd and diff --git a/Sources/SourceKitLSP/Swift/MacroExpansion.swift b/Sources/SwiftLanguageService/MacroExpansion.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/MacroExpansion.swift rename to Sources/SwiftLanguageService/MacroExpansion.swift index 74749100f..64d05436b 100644 --- a/Sources/SourceKitLSP/Swift/MacroExpansion.swift +++ b/Sources/SwiftLanguageService/MacroExpansion.swift @@ -18,6 +18,7 @@ import SKLogging import SKOptions import SKUtilities import SourceKitD +import SourceKitLSP import SwiftExtensions /// Caches the contents of macro expansions that were recently requested by the user. diff --git a/Sources/SourceKitLSP/Swift/OpenInterface.swift b/Sources/SwiftLanguageService/OpenInterface.swift similarity index 98% rename from Sources/SourceKitLSP/Swift/OpenInterface.swift rename to Sources/SwiftLanguageService/OpenInterface.swift index 973643712..f0921580c 100644 --- a/Sources/SourceKitLSP/Swift/OpenInterface.swift +++ b/Sources/SwiftLanguageService/OpenInterface.swift @@ -13,6 +13,7 @@ import Foundation package import LanguageServerProtocol import SKLogging +package import SourceKitLSP extension SwiftLanguageService { package func openGeneratedInterface( diff --git a/Sources/SourceKitLSP/Swift/RefactoringEdit.swift b/Sources/SwiftLanguageService/RefactoringEdit.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/RefactoringEdit.swift rename to Sources/SwiftLanguageService/RefactoringEdit.swift diff --git a/Sources/SourceKitLSP/Swift/RefactoringResponse.swift b/Sources/SwiftLanguageService/RefactoringResponse.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/RefactoringResponse.swift rename to Sources/SwiftLanguageService/RefactoringResponse.swift index 804f5e9e7..45b1a5672 100644 --- a/Sources/SourceKitLSP/Swift/RefactoringResponse.swift +++ b/Sources/SwiftLanguageService/RefactoringResponse.swift @@ -15,6 +15,7 @@ import LanguageServerProtocol import SKLogging import SKUtilities import SourceKitD +import SourceKitLSP protocol RefactoringResponse { init(title: String, uri: DocumentURI, refactoringEdits: [RefactoringEdit]) diff --git a/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift b/Sources/SwiftLanguageService/RelatedIdentifiers.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift rename to Sources/SwiftLanguageService/RelatedIdentifiers.swift index 89ba66848..3764487f3 100644 --- a/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift +++ b/Sources/SwiftLanguageService/RelatedIdentifiers.swift @@ -14,6 +14,7 @@ import Csourcekitd import LanguageServerProtocol import SKLogging import SourceKitD +import SourceKitLSP struct RelatedIdentifier { let range: Range diff --git a/Sources/SwiftLanguageService/Rename.swift b/Sources/SwiftLanguageService/Rename.swift new file mode 100644 index 000000000..26195782f --- /dev/null +++ b/Sources/SwiftLanguageService/Rename.swift @@ -0,0 +1,953 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 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 Csourcekitd +import Foundation +package import IndexStoreDB +package import LanguageServerProtocol +import SKLogging +import SKUtilities +import SourceKitD +package import SourceKitLSP +import SwiftExtensions +import SwiftSyntax + +// MARK: - Helper types + +/// A parsed representation of a name that may be disambiguated by its argument labels. +/// +/// ### Examples +/// - `foo(a:b:)` +/// - `foo(_:b:)` +/// - `foo` if no argument labels are specified, eg. for a variable. +private struct CompoundDeclName { + /// The parameter of a compound decl name, which can either be the parameter's name or `_` to indicate that the + /// parameter is unnamed. + enum Parameter: Equatable { + case named(String) + case wildcard + + var stringOrWildcard: String { + switch self { + case .named(let str): return str + case .wildcard: return "_" + } + } + + var stringOrEmpty: String { + switch self { + case .named(let str): return str + case .wildcard: return "" + } + } + } + + let baseName: String + let parameters: [Parameter] + + /// Parse a compound decl name into its base names and parameters. + init(_ compoundDeclName: String) { + guard let openParen = compoundDeclName.firstIndex(of: "(") else { + // We don't have a compound name. Everything is the base name + self.baseName = compoundDeclName + self.parameters = [] + return + } + self.baseName = String(compoundDeclName[.. Int` + case keywordBaseName + + /// The internal parameter name (aka. second name) inside a function declaration + /// + /// ### Examples + /// - ` b` in `func foo(a b: Int)` + case parameterName + + /// Same as `parameterName` but cannot be removed if it is the same as the parameter's first name. This only happens + /// for subscripts where parameters are unnamed by default unless they have both a first and second name. + /// + /// ### Examples + /// The second ` a` in `subscript(a a: Int)` + case noncollapsibleParameterName + + /// The external argument label of a function parameter + /// + /// ### Examples + /// - `a` in `func foo(a b: Int)` + /// - `a` in `func foo(a: Int)` + case declArgumentLabel + + /// The argument label inside a call. + /// + /// ### Examples + /// - `a` in `foo(a: 1)` + case callArgumentLabel + + /// The colon after an argument label inside a call. This is reported so it can be removed if the parameter becomes + /// unnamed. + /// + /// ### Examples + /// - `: ` in `foo(a: 1)` + case callArgumentColon + + /// An empty range that point to the position before an unnamed argument. This is used to insert the argument label + /// if an unnamed parameter becomes named. + /// + /// ### Examples + /// - An empty range before `1` in `foo(1)`, which could expand to `foo(a: 1)` + case callArgumentCombined + + /// The argument label in a compound decl name. + /// + /// ### Examples + /// - `a` in `foo(a:)` + case selectorArgumentLabel + + init?(_ uid: sourcekitd_api_uid_t, values: sourcekitd_api_values) { + switch uid { + case values.renameRangeBase: self = .baseName + case values.renameRangeCallArgColon: self = .callArgumentColon + case values.renameRangeCallArgCombined: self = .callArgumentCombined + case values.renameRangeCallArgLabel: self = .callArgumentLabel + case values.renameRangeDeclArgLabel: self = .declArgumentLabel + case values.renameRangeKeywordBase: self = .keywordBaseName + case values.renameRangeNoncollapsibleParam: self = .noncollapsibleParameterName + case values.renameRangeParam: self = .parameterName + case values.renameRangeSelectorArgLabel: self = .selectorArgumentLabel + default: return nil + } + } +} + +/// A single “piece” that is used for renaming a compound function name. +/// +/// See `SyntacticRenamePieceKind` for the different rename pieces that exist. +/// +/// ### Example +/// `foo(x: 1)` is represented by three pieces +/// - The base name `foo` +/// - The parameter name `x` +/// - The call argument colon `: `. +private struct SyntacticRenamePiece { + /// The range that represents this piece of the name + let range: Range + + /// The kind of the rename piece. + let kind: SyntacticRenamePieceKind + + /// If this piece belongs to a parameter, the index of that parameter (zero-based) or `nil` if this is the base name + /// piece. + let parameterIndex: Int? + + /// Create a `SyntacticRenamePiece` from a `sourcekitd` response. + init?( + _ dict: SKDResponseDictionary, + in snapshot: DocumentSnapshot, + keys: sourcekitd_api_keys, + values: sourcekitd_api_values + ) { + guard let line: Int = dict[keys.line], + let column: Int = dict[keys.column], + let endLine: Int = dict[keys.endLine], + let endColumn: Int = dict[keys.endColumn], + let kind: sourcekitd_api_uid_t = dict[keys.kind] + else { + return nil + } + let start = snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: column - 1) + let end = snapshot.positionOf(zeroBasedLine: endLine - 1, utf8Column: endColumn - 1) + guard let kind = SyntacticRenamePieceKind(kind, values: values) else { + return nil + } + + self.range = start.., callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Substring { + let start = self.stringIndexOf( + line: range.lowerBound.line, + utf16Column: range.lowerBound.utf16index, + callerFile: callerFile, + callerLine: callerLine + ) + let end = self.stringIndexOf( + line: range.upperBound.line, + utf16Column: range.upperBound.utf16index, + callerFile: callerFile, + callerLine: callerLine + ) + return self.content[start..(as syntaxType: S.Type) -> S? { + return parent?.as(S.self) + } +} + +fileprivate extension RelatedIdentifiersResponse { + func renameLocations(in snapshot: DocumentSnapshot) -> [RenameLocation] { + return self.relatedIdentifiers.map { + (relatedIdentifier) -> RenameLocation in + let position = relatedIdentifier.range.lowerBound + let utf8Column = snapshot.lineTable.utf8ColumnAt(line: position.line, utf16Column: position.utf16index) + return RenameLocation(line: position.line + 1, utf8Column: utf8Column + 1, usage: relatedIdentifier.usage) + } + } +} + +// MARK: - Name translation + +extension SwiftLanguageService: NameTranslatorService { + enum NameTranslationError: Error, CustomStringConvertible { + case malformedSwiftToClangTranslateNameResponse(SKDResponseDictionary) + case malformedClangToSwiftTranslateNameResponse(SKDResponseDictionary) + + var description: String { + switch self { + case .malformedSwiftToClangTranslateNameResponse(let response): + return """ + Malformed response for Swift to Clang name translation + + \(response.description) + """ + case .malformedClangToSwiftTranslateNameResponse(let response): + return """ + Malformed response for Clang to Swift name translation + + \(response.description) + """ + } + } + } + + /// Translate a Swift name to the corresponding C/C++/ObjectiveC name. + /// + /// This invokes the clang importer to perform the name translation, based on the `position` and `uri` at which the + /// Swift symbol is defined. + /// + /// - Parameters: + /// - position: The position at which the Swift name is defined + /// - uri: The URI of the document in which the Swift name is defined + /// - name: The Swift name of the symbol + package func translateSwiftNameToClang( + at symbolLocation: SymbolLocation, + in uri: DocumentURI, + name: String + ) async throws -> String { + let name = CompoundDeclName(name) + guard let snapshot = try? documentManager.latestSnapshotOrDisk(uri, language: .swift) else { + throw ResponseError.unknown("Failed to get contents of \(uri.forLogging) to translate Swift name to clang name") + } + + let req = sourcekitd.dictionary([ + keys.sourceFile: snapshot.uri.pseudoPath, + keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs + as [SKDRequestValue]?, + keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)), + keys.nameKind: sourcekitd.values.nameSwift, + keys.baseName: name.baseName, + keys.argNames: sourcekitd.array(name.parameters.map { $0.stringOrWildcard }), + ]) + + let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) + + guard let isZeroArgSelector: Int = response[keys.isZeroArgSelector], + let selectorPieces: SKDResponseArray = response[keys.selectorPieces] + else { + throw NameTranslationError.malformedSwiftToClangTranslateNameResponse(response) + } + return + try selectorPieces + .map { (dict: SKDResponseDictionary) -> String in + guard var name: String = dict[keys.name] else { + throw NameTranslationError.malformedSwiftToClangTranslateNameResponse(response) + } + if isZeroArgSelector == 0 { + // Selector pieces in multi-arg selectors end with ":" + name.append(":") + } + return name + }.joined() + } + + /// Translates a C/C++/Objective-C symbol name to Swift. + /// + /// This requires the position at which the the symbol is referenced in Swift so sourcekitd can determine the + /// clang declaration that is being renamed and check if that declaration has a `SWIFT_NAME`. If it does, this + /// `SWIFT_NAME` is used as the name translation result instead of invoking the clang importer rename rules. + /// + /// - Parameters: + /// - position: A position at which this symbol is referenced from Swift. + /// - snapshot: The snapshot containing the `position` that points to a usage of the clang symbol. + /// - isObjectiveCSelector: Whether the name is an Objective-C selector. Cannot be inferred from the name because + /// a name without `:` can also be a zero-arg Objective-C selector. For such names sourcekitd needs to know + /// whether it is translating a selector to apply the correct renaming rule. + /// - name: The clang symbol name. + /// - Returns: + package func translateClangNameToSwift( + at symbolLocation: SymbolLocation, + in snapshot: DocumentSnapshot, + isObjectiveCSelector: Bool, + name: String + ) async throws -> String { + let req = sourcekitd.dictionary([ + keys.sourceFile: snapshot.uri.pseudoPath, + keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs + as [SKDRequestValue]?, + keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)), + keys.nameKind: sourcekitd.values.nameObjc, + ]) + + if isObjectiveCSelector { + // Split the name into selector pieces, keeping the ':'. + let selectorPieces = name.split(separator: ":").map { String($0 + ":") } + req.set(keys.selectorPieces, to: sourcekitd.array(selectorPieces)) + } else { + req.set(keys.baseName, to: name) + } + + let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) + + guard let baseName: String = response[keys.baseName] else { + throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response) + } + let argNamesArray: SKDResponseArray? = response[keys.argNames] + let argNames = try argNamesArray?.map { (dict: SKDResponseDictionary) -> String in + guard var name: String = dict[keys.name] else { + throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response) + } + if name.isEmpty { + // Empty argument names are represented by `_` in Swift. + name = "_" + } + return name + ":" + } + var result = baseName + if let argNames, !argNames.isEmpty { + result += "(" + argNames.joined() + ")" + } + return result + } +} + +// MARK: - Rename + +extension SwiftLanguageService { + /// From a list of rename locations compute the list of `SyntacticRenameName`s that define which ranges need to be + /// edited to rename a compound decl name. + /// + /// - Parameters: + /// - renameLocations: The locations to rename + /// - oldName: The compound decl name that the declaration had before the rename. Used to verify that the rename + /// locations match that name. Eg. `myFunc(argLabel:otherLabel:)` or `myVar` + /// - snapshot: A `DocumentSnapshot` containing the contents of the file for which to compute the rename ranges. + private func getSyntacticRenameRanges( + renameLocations: [RenameLocation], + oldName: String, + in snapshot: DocumentSnapshot + ) async throws -> [SyntacticRenameName] { + let locations = sourcekitd.array( + renameLocations.map { renameLocation in + let location = sourcekitd.dictionary([ + keys.line: renameLocation.line, + keys.column: renameLocation.utf8Column, + keys.nameType: renameLocation.usage.uid(values: values), + ]) + return sourcekitd.dictionary([ + keys.locations: [location], + keys.name: oldName, + ]) + } + ) + + let skreq = sourcekitd.dictionary([ + keys.sourceFile: snapshot.uri.pseudoPath, + // find-syntactic-rename-ranges is a syntactic sourcekitd request that doesn't use the in-memory file snapshot. + // We need to send the source text again. + keys.sourceText: snapshot.text, + keys.renameLocations: locations, + ]) + + let syntacticRenameRangesResponse = try await send(sourcekitdRequest: \.findRenameRanges, skreq, snapshot: snapshot) + guard let categorizedRanges: SKDResponseArray = syntacticRenameRangesResponse[keys.categorizedRanges] else { + throw ResponseError.internalError("sourcekitd did not return categorized ranges") + } + + return categorizedRanges.compactMap { SyntacticRenameName($0, in: snapshot, keys: keys, values: values) } + } + + /// If `position` is on an argument label or a parameter name, find the range from the function's base name to the + /// token that terminates the arguments or parameters of the function. Typically, this is the closing ')' but it can + /// also be a closing ']' for subscripts or the end of a trailing closure. + private func findFunctionLikeRange(of position: Position, in snapshot: DocumentSnapshot) async -> Range? { + let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot) + guard let token = tree.token(at: snapshot.absolutePosition(of: position)) else { + return nil + } + + // The node that contains the function's base name. This might be an expression like `self.doStuff`. + // The start position of the last token in this node will be used as the base name position. + var startToken: TokenSyntax? = nil + var endToken: TokenSyntax? = nil + + switch token.keyPathInParent { + case \LabeledExprSyntax.label: + let callLike = token.parent(as: LabeledExprSyntax.self)?.parent(as: LabeledExprListSyntax.self)?.parent + switch callLike?.as(SyntaxEnum.self) { + case .attribute(let attribute): + startToken = attribute.attributeName.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) + endToken = attribute.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) + case .functionCallExpr(let functionCall): + startToken = functionCall.calledExpression.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) + endToken = functionCall.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) + case .macroExpansionDecl(let macroExpansionDecl): + startToken = macroExpansionDecl.macroName + endToken = macroExpansionDecl.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) + case .macroExpansionExpr(let macroExpansionExpr): + startToken = macroExpansionExpr.macroName + endToken = macroExpansionExpr.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) + case .subscriptCallExpr(let subscriptCall): + startToken = subscriptCall.leftSquare + endToken = subscriptCall.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) + default: + break + } + case \FunctionParameterSyntax.firstName: + let parameterClause = + token + .parent(as: FunctionParameterSyntax.self)? + .parent(as: FunctionParameterListSyntax.self)? + .parent(as: FunctionParameterClauseSyntax.self) + if let functionSignature = parameterClause?.parent(as: FunctionSignatureSyntax.self) { + switch functionSignature.parent?.as(SyntaxEnum.self) { + case .functionDecl(let functionDecl): + startToken = functionDecl.name + endToken = functionSignature.parameterClause.rightParen + case .initializerDecl(let initializerDecl): + startToken = initializerDecl.initKeyword + endToken = functionSignature.parameterClause.rightParen + case .macroDecl(let macroDecl): + startToken = macroDecl.name + endToken = functionSignature.parameterClause.rightParen + default: + break + } + } else if let subscriptDecl = parameterClause?.parent(as: SubscriptDeclSyntax.self) { + startToken = subscriptDecl.subscriptKeyword + endToken = subscriptDecl.parameterClause.rightParen + } + case \DeclNameArgumentSyntax.name: + let declReference = + token + .parent(as: DeclNameArgumentSyntax.self)? + .parent(as: DeclNameArgumentListSyntax.self)? + .parent(as: DeclNameArgumentsSyntax.self)? + .parent(as: DeclReferenceExprSyntax.self) + startToken = declReference?.baseName + endToken = declReference?.argumentNames?.rightParen + default: + break + } + + if let startToken, let endToken { + return snapshot.absolutePositionRange( + of: startToken.positionAfterSkippingLeadingTrivia.. (position: Position?, usr: String?, functionLikeRange: Range?) { + let startOfIdentifierPosition = await adjustPositionToStartOfIdentifier(position, in: snapshot) + let symbolInfo = try? await self.symbolInfo( + SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: startOfIdentifierPosition) + ) + + guard let functionLikeRange = await findFunctionLikeRange(of: startOfIdentifierPosition, in: snapshot) else { + return (startOfIdentifierPosition, symbolInfo?.only?.usr, nil) + } + if let onlySymbol = symbolInfo?.only, onlySymbol.kind == .constructor { + // We have a rename like `MyStruct(x: 1)`, invoked from `x`. + if let bestLocalDeclaration = onlySymbol.bestLocalDeclaration, bestLocalDeclaration.uri == snapshot.uri { + // If the initializer is declared within the same file, we can perform rename in the current file based on + // the declaration's location. + return (bestLocalDeclaration.range.lowerBound, onlySymbol.usr, functionLikeRange) + } + // Otherwise, we don't have a reference to the base name of the initializer and we can't use related + // identifiers to perform the rename. + // Return `nil` for the position to perform a pure index-based rename. + return (nil, onlySymbol.usr, functionLikeRange) + } + // Adjust the symbol info to the symbol info of the base name. + // This ensures that we get the symbol info of the function's base instead of the parameter. + let baseNameSymbolInfo = try? await self.symbolInfo( + SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: functionLikeRange.lowerBound) + ) + return (functionLikeRange.lowerBound, baseNameSymbolInfo?.only?.usr, functionLikeRange) + } + + package func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) { + let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri) + + let (renamePosition, usr, _) = await symbolToRename(at: request.position, in: snapshot) + guard let renamePosition else { + return (edits: WorkspaceEdit(), usr: usr) + } + + let relatedIdentifiersResponse = try await self.relatedIdentifiers( + at: renamePosition, + in: snapshot, + includeNonEditableBaseNames: true + ) + guard let oldNameString = relatedIdentifiersResponse.name else { + throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename") + } + + let renameLocations = relatedIdentifiersResponse.renameLocations(in: snapshot) + + try Task.checkCancellation() + + let oldName = CrossLanguageName(clangName: nil, swiftName: oldNameString, definitionLanguage: .swift) + let newName = CrossLanguageName(clangName: nil, swiftName: request.newName, definitionLanguage: .swift) + var edits = try await editsToRename( + locations: renameLocations, + in: snapshot, + oldName: oldName, + newName: newName + ) + if let compoundSwiftName = oldName.compoundSwiftName, !compoundSwiftName.parameters.isEmpty { + // If we are doing a function rename, run `renameParametersInFunctionBody` for every occurrence of the rename + // location within the current file. If the location is not a function declaration, it will exit early without + // invoking sourcekitd, so it's OK to do this performance-wise. + for renameLocation in renameLocations { + edits += await editsToRenameParametersInFunctionBody( + snapshot: snapshot, + renameLocation: renameLocation, + newName: newName + ) + } + } + edits = edits.filter { !$0.isNoOp(in: snapshot) } + + if edits.isEmpty { + return (edits: WorkspaceEdit(changes: [:]), usr: usr) + } + return (edits: WorkspaceEdit(changes: [snapshot.uri: edits]), usr: usr) + } + + package func editsToRenameParametersInFunctionBody( + snapshot: DocumentSnapshot, + renameLocation: RenameLocation, + newName: CrossLanguageName + ) async -> [TextEdit] { + let position = snapshot.absolutePosition(of: renameLocation) + let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) + let token = syntaxTree.token(at: position) + let parameterClause: FunctionParameterClauseSyntax? + switch token?.keyPathInParent { + case \FunctionDeclSyntax.name: + parameterClause = token?.parent(as: FunctionDeclSyntax.self)?.signature.parameterClause + case \InitializerDeclSyntax.initKeyword: + parameterClause = token?.parent(as: InitializerDeclSyntax.self)?.signature.parameterClause + case \SubscriptDeclSyntax.subscriptKeyword: + parameterClause = token?.parent(as: SubscriptDeclSyntax.self)?.parameterClause + default: + parameterClause = nil + } + guard let parameterClause else { + // We are not at a function-like definition. Nothing to rename. + return [] + } + guard let newSwiftNameString = newName.swiftName else { + logger.fault( + "Cannot rename at \(renameLocation.line):\(renameLocation.utf8Column) because new name is not a Swift name" + ) + return [] + } + let newSwiftName = CompoundDeclName(newSwiftNameString) + + var edits: [TextEdit] = [] + for (index, parameter) in parameterClause.parameters.enumerated() { + guard parameter.secondName == nil else { + // The parameter has a second name. The function signature only renames the first name and the function body + // refers to the second name. Nothing to do. + continue + } + let oldParameterName = parameter.firstName.text + guard index < newSwiftName.parameters.count else { + // We don't have a new name for this parameter. Nothing to do. + continue + } + let newParameterName = newSwiftName.parameters[index].stringOrEmpty + guard !newParameterName.isEmpty else { + // We are changing the parameter to an empty name. This will retain the current external parameter name as the + // new second name, so nothing to do in the function body. + continue + } + guard newParameterName != oldParameterName else { + // This parameter wasn't modified. Nothing to do. + continue + } + + let oldCrossLanguageParameterName = CrossLanguageName( + clangName: nil, + swiftName: oldParameterName, + definitionLanguage: .swift + ) + let newCrossLanguageParameterName = CrossLanguageName( + clangName: nil, + swiftName: newParameterName, + definitionLanguage: .swift + ) + + let parameterRenameEdits = await orLog("Renaming parameter") { + let parameterPosition = snapshot.position(of: parameter.positionAfterSkippingLeadingTrivia) + // Once we have lexical scope lookup in swift-syntax, this can be a purely syntactic rename. + // We know that the parameters are variables and thus there can't be overloads that need to be resolved by the + // type checker. + let relatedIdentifiers = try await self.relatedIdentifiers( + at: parameterPosition, + in: snapshot, + includeNonEditableBaseNames: false + ) + + // Exclude the edit that renames the parameter itself. The parameter gets renamed as part of the function + // declaration. + let filteredRelatedIdentifiers = RelatedIdentifiersResponse( + relatedIdentifiers: relatedIdentifiers.relatedIdentifiers.filter { !$0.range.contains(parameterPosition) }, + name: relatedIdentifiers.name + ) + + let parameterRenameLocations = filteredRelatedIdentifiers.renameLocations(in: snapshot) + + return try await editsToRename( + locations: parameterRenameLocations, + in: snapshot, + oldName: oldCrossLanguageParameterName, + newName: newCrossLanguageParameterName + ) + } + guard let parameterRenameEdits else { + continue + } + edits += parameterRenameEdits + } + return edits + } + + /// Return the edit that needs to be performed for the given syntactic rename piece to rename it from + /// `oldParameter` to `newParameter`. + /// Returns `nil` if no edit needs to be performed. + private func textEdit( + for piece: SyntacticRenamePiece, + in snapshot: DocumentSnapshot, + oldParameter: CompoundDeclName.Parameter, + newParameter: CompoundDeclName.Parameter + ) -> TextEdit? { + switch piece.kind { + case .parameterName: + if newParameter == .wildcard, piece.range.isEmpty, case .named(let oldParameterName) = oldParameter { + // We are changing a named parameter to an unnamed one. If the parameter didn't have an internal parameter + // name, we need to transfer the previously external parameter name to be the internal one. + // E.g. `func foo(a: Int)` becomes `func foo(_ a: Int)`. + return TextEdit(range: piece.range, newText: " " + oldParameterName) + } + if case .named(let newParameterLabel) = newParameter, + newParameterLabel.trimmingCharacters(in: .whitespaces) + == snapshot.lineTable[piece.range].trimmingCharacters(in: .whitespaces) + { + // We are changing the external parameter name to be the same one as the internal parameter name. The + // internal name is thus no longer needed. Drop it. + // Eg. an old declaration `func foo(_ a: Int)` becomes `func foo(a: Int)` when renaming the parameter to `a` + return TextEdit(range: piece.range, newText: "") + } + // In all other cases, don't touch the internal parameter name. It's not part of the public API. + return nil + case .noncollapsibleParameterName: + // Noncollapsible parameter names should never be renamed because they are the same as `parameterName` but + // never fall into one of the two categories above. + return nil + case .declArgumentLabel: + if piece.range.isEmpty { + // If we are inserting a new external argument label where there wasn't one before, add a space after it to + // separate it from the internal name. + // E.g. `subscript(a: Int)` becomes `subscript(a a: Int)`. + return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard + " ") + } + // Otherwise, just update the name. + return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard) + case .callArgumentLabel: + // Argument labels of calls are just updated. + return TextEdit(range: piece.range, newText: newParameter.stringOrEmpty) + case .callArgumentColon: + if case .wildcard = newParameter { + // If the parameter becomes unnamed, remove the colon after the argument name. + return TextEdit(range: piece.range, newText: "") + } + return nil + case .callArgumentCombined: + if case .named(let newParameterName) = newParameter { + // If an unnamed parameter becomes named, insert the new name and a colon. + return TextEdit(range: piece.range, newText: newParameterName + ": ") + } + return nil + case .selectorArgumentLabel: + return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard) + case .baseName, .keywordBaseName: + preconditionFailure("Handled above") + } + } + + package func editsToRename( + locations renameLocations: [RenameLocation], + in snapshot: DocumentSnapshot, + oldName oldCrossLanguageName: CrossLanguageName, + newName newCrossLanguageName: CrossLanguageName + ) async throws -> [TextEdit] { + guard + let oldNameString = oldCrossLanguageName.swiftName, + let oldName = oldCrossLanguageName.compoundSwiftName, + let newName = newCrossLanguageName.compoundSwiftName + else { + throw ResponseError.unknown( + "Failed to rename \(snapshot.uri.forLogging) because the Swift name for rename is unknown" + ) + } + + let tree = await syntaxTreeManager.syntaxTree(for: snapshot) + + let compoundRenameRanges = try await getSyntacticRenameRanges( + renameLocations: renameLocations, + oldName: oldNameString, + in: snapshot + ) + + try Task.checkCancellation() + + return compoundRenameRanges.flatMap { (compoundRenameRange) -> [TextEdit] in + switch compoundRenameRange.category { + case .unmatched, .mismatch: + // The location didn't match. Don't rename it + return [] + case .activeCode, .inactiveCode, .selector: + // Occurrences in active code and selectors should always be renamed. + // Inactive code is currently never returned by sourcekitd. + break + case .string, .comment: + // We currently never get any results in strings or comments because the related identifiers request doesn't + // provide any locations inside strings or comments. We would need to have a textual index to find these + // locations. + return [] + } + return compoundRenameRange.pieces.compactMap { (piece) -> TextEdit? in + if piece.kind == .baseName { + if let firstNameToken = tree.token(at: snapshot.absolutePosition(of: piece.range.lowerBound)), + firstNameToken.keyPathInParent == \FunctionParameterSyntax.firstName, + let parameterSyntax = firstNameToken.parent(as: FunctionParameterSyntax.self), + parameterSyntax.secondName == nil // Should always be true because otherwise decl would be second name + { + // We are renaming a function parameter from inside the function body. + // This should be a local rename and it shouldn't affect all the callers of the function. Introduce the new + // name as a second name. + return TextEdit( + range: Range(snapshot.position(of: firstNameToken.endPositionBeforeTrailingTrivia)), + newText: " " + newName.baseName + ) + } + + return TextEdit(range: piece.range, newText: newName.baseName) + } else if piece.kind == .keywordBaseName { + // Keyword base names can't be renamed + return nil + } + + guard let parameterIndex = piece.parameterIndex, + parameterIndex < newName.parameters.count, + parameterIndex < oldName.parameters.count + else { + // Be lenient and just keep the old parameter names if the new name doesn't specify them, eg. if we are + // renaming `func foo(a: Int, b: Int)` and the user specified `bar(x:)` as the new name. + return nil + } + + return self.textEdit( + for: piece, + in: snapshot, + oldParameter: oldName.parameters[parameterIndex], + newParameter: newName.parameters[parameterIndex] + ) + } + } + } + + package func prepareRename( + _ request: PrepareRenameRequest + ) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? { + let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri) + + let (renamePosition, usr, functionLikeRange) = await symbolToRename(at: request.position, in: snapshot) + guard let renamePosition else { + return nil + } + + let response = try await self.relatedIdentifiers( + at: renamePosition, + in: snapshot, + includeNonEditableBaseNames: true + ) + guard var name = response.name else { + throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename") + } + if name.hasSuffix("()") { + name = String(name.dropLast(2)) + } + guard let relatedIdentRange = response.relatedIdentifiers.first(where: { $0.range.contains(renamePosition) })?.range + else { + return nil + } + return (PrepareRenameResponse(range: functionLikeRange ?? relatedIdentRange, placeholder: name), usr) + } +} diff --git a/Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift b/Sources/SwiftLanguageService/RewriteSourceKitPlaceholders.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/RewriteSourceKitPlaceholders.swift rename to Sources/SwiftLanguageService/RewriteSourceKitPlaceholders.swift diff --git a/Sources/SourceKitLSP/Swift/SemanticRefactorCommand.swift b/Sources/SwiftLanguageService/SemanticRefactorCommand.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/SemanticRefactorCommand.swift rename to Sources/SwiftLanguageService/SemanticRefactorCommand.swift diff --git a/Sources/SourceKitLSP/Swift/SemanticRefactoring.swift b/Sources/SwiftLanguageService/SemanticRefactoring.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/SemanticRefactoring.swift rename to Sources/SwiftLanguageService/SemanticRefactoring.swift index feb78061f..0b321faa1 100644 --- a/Sources/SourceKitLSP/Swift/SemanticRefactoring.swift +++ b/Sources/SwiftLanguageService/SemanticRefactoring.swift @@ -13,6 +13,7 @@ import LanguageServerProtocol import SKLogging import SourceKitD +import SourceKitLSP /// Detailed information about the result of a specific refactoring operation. /// diff --git a/Sources/SourceKitLSP/Swift/SemanticTokens.swift b/Sources/SwiftLanguageService/SemanticTokens.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/SemanticTokens.swift rename to Sources/SwiftLanguageService/SemanticTokens.swift index a23255aaa..010d299eb 100644 --- a/Sources/SourceKitLSP/Swift/SemanticTokens.swift +++ b/Sources/SwiftLanguageService/SemanticTokens.swift @@ -13,6 +13,7 @@ package import LanguageServerProtocol import SKLogging import SourceKitD +import SourceKitLSP import SwiftIDEUtils import SwiftParser import SwiftSyntax diff --git a/Sources/SourceKitLSP/Swift/SwiftCodeLensScanner.swift b/Sources/SwiftLanguageService/SwiftCodeLensScanner.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/SwiftCodeLensScanner.swift rename to Sources/SwiftLanguageService/SwiftCodeLensScanner.swift index 28cd6115a..8c2647815 100644 --- a/Sources/SourceKitLSP/Swift/SwiftCodeLensScanner.swift +++ b/Sources/SwiftLanguageService/SwiftCodeLensScanner.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import LanguageServerProtocol +import SourceKitLSP import SwiftSyntax /// Scans a source file for classes or structs annotated with `@main` and returns a code lens for them. diff --git a/Sources/SourceKitLSP/Swift/SwiftCommand.swift b/Sources/SwiftLanguageService/SwiftCommand.swift similarity index 85% rename from Sources/SourceKitLSP/Swift/SwiftCommand.swift rename to Sources/SwiftLanguageService/SwiftCommand.swift index 99f3f30b0..54677077f 100644 --- a/Sources/SourceKitLSP/Swift/SwiftCommand.swift +++ b/Sources/SwiftLanguageService/SwiftCommand.swift @@ -12,16 +12,6 @@ package import LanguageServerProtocol -/// The set of known Swift commands. -/// -/// All commands from the Swift LSP should be listed here. -package let builtinSwiftCommands: [String] = [ - SemanticRefactorCommand.self, - ExpandMacroCommand.self, -].map { (command: any SwiftCommand.Type) in - command.identifier -} - /// A `Command` that should be executed by Swift's language server. package protocol SwiftCommand: Codable, Hashable, LSPAnyCodable { static var identifier: String { get } @@ -55,3 +45,14 @@ extension ExecuteCommandRequest { return type.init(fromLSPDictionary: dictionary) } } + +extension SwiftLanguageService { + package static var builtInCommands: [String] { + [ + SemanticRefactorCommand.self, + ExpandMacroCommand.self, + ].map { (command: any SwiftCommand.Type) in + command.identifier + } + } +} diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift similarity index 98% rename from Sources/SourceKitLSP/Swift/SwiftLanguageService.swift rename to Sources/SwiftLanguageService/SwiftLanguageService.swift index 69759612e..a70b291f7 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -23,6 +23,7 @@ package import SKOptions import SKUtilities import SemanticIndex package import SourceKitD +package import SourceKitLSP import SwiftExtensions import SwiftParser import SwiftParserDiagnostics @@ -401,7 +402,7 @@ extension SwiftLanguageService { colorProvider: .bool(true), foldingRangeProvider: .bool(true), executeCommandProvider: ExecuteCommandOptions( - commands: builtinSwiftCommands + commands: Self.builtInCommands ), semanticTokensProvider: SemanticTokensOptions( legend: SemanticTokensLegend.sourceKitLSPLegend, @@ -1189,6 +1190,22 @@ struct SourceKitDPosition { public var utf8Column: Int } +extension DocumentSnapshot { + func sourcekitdPosition( + of position: Position, + callerFile: StaticString = #fileID, + callerLine: UInt = #line + ) -> SourceKitDPosition { + let utf8Column = lineTable.utf8ColumnAt( + line: position.line, + utf16Column: position.utf16index, + callerFile: callerFile, + callerLine: callerLine + ) + return SourceKitDPosition(line: position.line + 1, utf8Column: utf8Column + 1) + } +} + extension sourcekitd_api_uid_t { func isCommentKind(_ vals: sourcekitd_api_values) -> Bool { switch self { diff --git a/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift b/Sources/SwiftLanguageService/SwiftTestingScanner.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift rename to Sources/SwiftLanguageService/SwiftTestingScanner.swift index 9ec11e622..67bac6124 100644 --- a/Sources/SourceKitLSP/Swift/SwiftTestingScanner.swift +++ b/Sources/SwiftLanguageService/SwiftTestingScanner.swift @@ -12,6 +12,7 @@ import LanguageServerProtocol import SKLogging +import SourceKitLSP import SwiftParser import SwiftSyntax diff --git a/Sources/SwiftLanguageService/SymbolGraph.swift b/Sources/SwiftLanguageService/SymbolGraph.swift new file mode 100644 index 000000000..e486a1979 --- /dev/null +++ b/Sources/SwiftLanguageService/SymbolGraph.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 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 +// +//===----------------------------------------------------------------------===// + +package import IndexStoreDB +package import LanguageServerProtocol +import SourceKitLSP + +extension SwiftLanguageService { + package func symbolGraph( + forOnDiskContentsOf symbolDocumentUri: DocumentURI, + at location: SymbolLocation + ) async throws -> String? { + return try await withSnapshotFromDiskOpenedInSourcekitd( + uri: symbolDocumentUri, + fallbackSettingsAfterTimeout: false + ) { snapshot, compileCommand in + try await cursorInfo( + snapshot, + compileCommand: compileCommand, + Range(snapshot.position(of: location)), + includeSymbolGraph: true + ).symbolGraph + } + } +} diff --git a/Sources/SourceKitLSP/Swift/SymbolInfo.swift b/Sources/SwiftLanguageService/SymbolInfo.swift similarity index 98% rename from Sources/SourceKitLSP/Swift/SymbolInfo.swift rename to Sources/SwiftLanguageService/SymbolInfo.swift index 5b1c97325..a6e0fed08 100644 --- a/Sources/SourceKitLSP/Swift/SymbolInfo.swift +++ b/Sources/SwiftLanguageService/SymbolInfo.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// package import LanguageServerProtocol +import SourceKitLSP extension SwiftLanguageService { package func symbolInfo(_ req: SymbolInfoRequest) async throws -> [SymbolDetails] { diff --git a/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift b/Sources/SwiftLanguageService/SyntacticSwiftXCTestScanner.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift rename to Sources/SwiftLanguageService/SyntacticSwiftXCTestScanner.swift index 30a23b8a4..eaeb1dae4 100644 --- a/Sources/SourceKitLSP/Swift/SyntacticSwiftXCTestScanner.swift +++ b/Sources/SwiftLanguageService/SyntacticSwiftXCTestScanner.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import LanguageServerProtocol +import SourceKitLSP import SwiftSyntax /// Scans a source file for `XCTestCase` classes and test methods. diff --git a/Sources/SourceKitLSP/Swift/SyntaxHighlightingToken.swift b/Sources/SwiftLanguageService/SyntaxHighlightingToken.swift similarity index 100% rename from Sources/SourceKitLSP/Swift/SyntaxHighlightingToken.swift rename to Sources/SwiftLanguageService/SyntaxHighlightingToken.swift diff --git a/Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift b/Sources/SwiftLanguageService/SyntaxHighlightingTokenParser.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift rename to Sources/SwiftLanguageService/SyntaxHighlightingTokenParser.swift index b6fa55511..7756fb778 100644 --- a/Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift +++ b/Sources/SwiftLanguageService/SyntaxHighlightingTokenParser.swift @@ -14,6 +14,7 @@ import Csourcekitd import LanguageServerProtocol import SKLogging import SourceKitD +package import SourceKitLSP /// Parses tokens from sourcekitd response dictionaries. struct SyntaxHighlightingTokenParser { diff --git a/Sources/SourceKitLSP/Swift/SyntaxHighlightingTokens.swift b/Sources/SwiftLanguageService/SyntaxHighlightingTokens.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/SyntaxHighlightingTokens.swift rename to Sources/SwiftLanguageService/SyntaxHighlightingTokens.swift index ece8636fb..a7dc1816a 100644 --- a/Sources/SourceKitLSP/Swift/SyntaxHighlightingTokens.swift +++ b/Sources/SwiftLanguageService/SyntaxHighlightingTokens.swift @@ -13,6 +13,7 @@ import LanguageServerProtocol import SKLogging import SourceKitD +import SourceKitLSP /// A wrapper around an array of syntax highlighting tokens. package struct SyntaxHighlightingTokens: Sendable { diff --git a/Sources/SourceKitLSP/Swift/SyntaxTreeManager.swift b/Sources/SwiftLanguageService/SyntaxTreeManager.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/SyntaxTreeManager.swift rename to Sources/SwiftLanguageService/SyntaxTreeManager.swift index 5509c4fc8..9a4e9af40 100644 --- a/Sources/SourceKitLSP/Swift/SyntaxTreeManager.swift +++ b/Sources/SwiftLanguageService/SyntaxTreeManager.swift @@ -12,6 +12,7 @@ import LanguageServerProtocol import SKUtilities +import SourceKitLSP import SwiftParser import SwiftSyntax diff --git a/Sources/SwiftLanguageService/TestDiscovery.swift b/Sources/SwiftLanguageService/TestDiscovery.swift new file mode 100644 index 000000000..8ed283d6b --- /dev/null +++ b/Sources/SwiftLanguageService/TestDiscovery.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import BuildServerIntegration +import BuildServerProtocol +import Foundation +package import LanguageServerProtocol +import SKLogging +import SemanticIndex +package import SourceKitLSP +import SwiftExtensions + +extension SwiftLanguageService { + package func syntacticDocumentTests( + for uri: DocumentURI, + in workspace: Workspace + ) async throws -> [AnnotatedTestItem]? { + let targetIdentifiers = await workspace.buildServerManager.targets(for: uri) + let isInTestTarget = await targetIdentifiers.asyncContains(where: { + await workspace.buildServerManager.buildTarget(named: $0)?.tags.contains(.test) ?? true + }) + if !targetIdentifiers.isEmpty && !isInTestTarget { + // If we know the targets for the file and the file is not part of any test target, don't scan it for tests. + return nil + } + let snapshot = try documentManager.latestSnapshot(uri) + let semanticSymbols = workspace.index(checkedFor: .deletedFiles)?.symbols(inFilePath: snapshot.uri.pseudoPath) + let xctestSymbols = await SyntacticSwiftXCTestScanner.findTestSymbols( + in: snapshot, + syntaxTreeManager: syntaxTreeManager + ) + .compactMap { $0.filterUsing(semanticSymbols: semanticSymbols) } + + let swiftTestingSymbols = await SyntacticSwiftTestingTestScanner.findTestSymbols( + in: snapshot, + syntaxTreeManager: syntaxTreeManager + ) + return (xctestSymbols + swiftTestingSymbols).sorted { $0.testItem.location < $1.testItem.location } + } + + package static func syntacticTestItems(in uri: DocumentURI) async -> [AnnotatedTestItem] { + guard let url = uri.fileURL else { + logger.log("Not indexing \(uri.forLogging) for tests because it is not a file URL") + return [] + } + let syntaxTreeManager = SyntaxTreeManager() + let snapshot = orLog("Getting document snapshot for syntactic Swift test scanning") { + try DocumentSnapshot(withContentsFromDisk: url, language: .swift) + } + guard let snapshot else { + return [] + } + async let swiftTestingTests = SyntacticSwiftTestingTestScanner.findTestSymbols( + in: snapshot, + syntaxTreeManager: syntaxTreeManager + ) + async let xcTests = SyntacticSwiftXCTestScanner.findTestSymbols(in: snapshot, syntaxTreeManager: syntaxTreeManager) + + return await swiftTestingTests + xcTests + } +} diff --git a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift b/Sources/SwiftLanguageService/VariableTypeInfo.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/VariableTypeInfo.swift rename to Sources/SwiftLanguageService/VariableTypeInfo.swift index 348913c2d..5badbac8f 100644 --- a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift +++ b/Sources/SwiftLanguageService/VariableTypeInfo.swift @@ -13,6 +13,7 @@ import Dispatch import LanguageServerProtocol import SourceKitD +import SourceKitLSP import SwiftSyntax fileprivate extension TokenSyntax { diff --git a/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift b/Sources/SwiftLanguageService/WithSnapshotFromDiskOpenedInSourcekitd.swift similarity index 99% rename from Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift rename to Sources/SwiftLanguageService/WithSnapshotFromDiskOpenedInSourcekitd.swift index 305c7c593..2a38725c5 100644 --- a/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift +++ b/Sources/SwiftLanguageService/WithSnapshotFromDiskOpenedInSourcekitd.swift @@ -15,6 +15,7 @@ import Foundation import LanguageServerProtocol import SKLogging import SKUtilities +import SourceKitLSP import SwiftExtensions extension SwiftLanguageService { diff --git a/Tests/SourceKitLSPTests/BuildServerTests.swift b/Tests/SourceKitLSPTests/BuildServerTests.swift index c0341726a..09be050ab 100644 --- a/Tests/SourceKitLSPTests/BuildServerTests.swift +++ b/Tests/SourceKitLSPTests/BuildServerTests.swift @@ -98,6 +98,7 @@ final class BuildServerTests: XCTestCase { self.workspace = await Workspace.forTesting( options: try .testDefault(), + sourceKitLSPServer: server, testHooks: Hooks(), buildServerManager: buildServerManager, indexTaskScheduler: .forTesting diff --git a/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift b/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift index bc3f95091..59516b760 100644 --- a/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift +++ b/Tests/SourceKitLSPTests/ClosureCompletionFormatTests.swift @@ -10,9 +10,10 @@ // //===----------------------------------------------------------------------===// -@_spi(Testing) import SourceKitLSP +import SourceKitLSP import Swift import SwiftBasicFormat +@_spi(Testing) import SwiftLanguageService import SwiftParser import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Tests/SourceKitLSPTests/CrashRecoveryTests.swift b/Tests/SourceKitLSPTests/CrashRecoveryTests.swift index 0d4fb7ea6..99120f2d9 100644 --- a/Tests/SourceKitLSPTests/CrashRecoveryTests.swift +++ b/Tests/SourceKitLSPTests/CrashRecoveryTests.swift @@ -16,8 +16,9 @@ import SKLogging import SKOptions import SKTestSupport import SourceKitD -@_spi(Testing) import SourceKitLSP +import SourceKitLSP import SwiftExtensions +@_spi(Testing) import SwiftLanguageService import XCTest fileprivate extension HoverResponse { diff --git a/Tests/SourceKitLSPTests/ExecuteCommandTests.swift b/Tests/SourceKitLSPTests/ExecuteCommandTests.swift index f8cef1fbb..4eaf60696 100644 --- a/Tests/SourceKitLSPTests/ExecuteCommandTests.swift +++ b/Tests/SourceKitLSPTests/ExecuteCommandTests.swift @@ -15,6 +15,7 @@ import SKOptions import SKTestSupport @_spi(Testing) import SourceKitLSP import SwiftExtensions +import SwiftLanguageService import XCTest final class ExecuteCommandTests: XCTestCase { diff --git a/Tests/SourceKitLSPTests/ExpandMacroTests.swift b/Tests/SourceKitLSPTests/ExpandMacroTests.swift index 1679f1b57..42749ccec 100644 --- a/Tests/SourceKitLSPTests/ExpandMacroTests.swift +++ b/Tests/SourceKitLSPTests/ExpandMacroTests.swift @@ -16,6 +16,7 @@ import SKOptions import SKTestSupport @_spi(Testing) import SourceKitLSP import SwiftExtensions +import SwiftLanguageService import XCTest final class ExpandMacroTests: XCTestCase { diff --git a/Tests/SourceKitLSPTests/LocalSwiftTests.swift b/Tests/SourceKitLSPTests/LocalSwiftTests.swift index e281365a7..1d127db5b 100644 --- a/Tests/SourceKitLSPTests/LocalSwiftTests.swift +++ b/Tests/SourceKitLSPTests/LocalSwiftTests.swift @@ -15,8 +15,9 @@ import SKLogging import SKOptions import SKTestSupport import SourceKitD -@_spi(Testing) import SourceKitLSP +import SourceKitLSP import SwiftExtensions +@_spi(Testing) import SwiftLanguageService import SwiftParser import SwiftSyntax import XCTest diff --git a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift index f7a3f8439..9d1efe685 100644 --- a/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift +++ b/Tests/SourceKitLSPTests/RewriteSourceKitPlaceholdersTests.swift @@ -11,7 +11,8 @@ //===----------------------------------------------------------------------===// import SKTestSupport -@_spi(Testing) import SourceKitLSP +import SourceKitLSP +@_spi(Testing) import SwiftLanguageService import XCTest final class RewriteSourceKitPlaceholdersTests: XCTestCase { diff --git a/Tests/SourceKitLSPTests/SemanticTokensTests.swift b/Tests/SourceKitLSPTests/SemanticTokensTests.swift index db0216bff..38d88fed4 100644 --- a/Tests/SourceKitLSPTests/SemanticTokensTests.swift +++ b/Tests/SourceKitLSPTests/SemanticTokensTests.swift @@ -16,6 +16,7 @@ import SKTestSupport import SKUtilities import SourceKitD @_spi(Testing) import SourceKitLSP +import SwiftLanguageService import XCTest private typealias Token = SyntaxHighlightingToken diff --git a/Tests/SourceKitLSPTests/SwiftCompileCommandsTest.swift b/Tests/SourceKitLSPTests/SwiftCompileCommandsTest.swift index ba4ac1d92..a96366967 100644 --- a/Tests/SourceKitLSPTests/SwiftCompileCommandsTest.swift +++ b/Tests/SourceKitLSPTests/SwiftCompileCommandsTest.swift @@ -13,6 +13,7 @@ import BuildServerIntegration import LanguageServerProtocol import SourceKitLSP +import SwiftLanguageService import XCTest final class SwiftCompileCommandsTest: XCTestCase { diff --git a/Tests/SourceKitLSPTests/SyntaxRefactorTests.swift b/Tests/SourceKitLSPTests/SyntaxRefactorTests.swift index 460f07897..83fca5d5b 100644 --- a/Tests/SourceKitLSPTests/SyntaxRefactorTests.swift +++ b/Tests/SourceKitLSPTests/SyntaxRefactorTests.swift @@ -12,6 +12,7 @@ import SKTestSupport @_spi(Testing) import SourceKitLSP +import SwiftLanguageService import SwiftParser import SwiftRefactor import SwiftSyntax