Files
sourcekit-lsp/Sources/BuildServerProtocol/Messages/BuildTargetSourcesRequest.swift
Alex Hoppen 882c990cae Add support for copied header files to SourceKit-LSP
If a build server copies files (eg. header) to the build directory during preparation and those copied files are referenced for semantic functionality, we would currently jump to the file in the build directory. Teach SourceKit-LSP about files that are copied during preparation and if we detect that we are jumping to such a file, jump to the original file instead.

So far only the definition request checks the copied file paths. Adding support for copied file paths in the other requests will be a follow-up change.
2025-08-30 23:35:14 +02:00

220 lines
8.0 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
public import LanguageServerProtocol
/// The build target sources request is sent from the client to the server to
/// query for the list of text documents and directories that belong to a
/// build target. The sources response must not include sources that are
/// external to the workspace.
public struct BuildTargetSourcesRequest: RequestType, Hashable {
public static let method: String = "buildTarget/sources"
public typealias Response = BuildTargetSourcesResponse
public var targets: [BuildTargetIdentifier]
public init(targets: [BuildTargetIdentifier]) {
self.targets = targets
}
}
public struct BuildTargetSourcesResponse: ResponseType, Hashable {
public var items: [SourcesItem]
public init(items: [SourcesItem]) {
self.items = items
}
}
public struct SourcesItem: Codable, Hashable, Sendable {
public var target: BuildTargetIdentifier
/// The text documents and directories that belong to this build target.
public var sources: [SourceItem]
/// The root directories from where source files should be relativized.
/// Example: ["file://Users/name/dev/metals/src/main/scala"]
public var roots: [URI]?
public init(target: BuildTargetIdentifier, sources: [SourceItem], roots: [URI]? = nil) {
self.target = target
self.sources = sources
self.roots = roots
}
}
public struct SourceItem: Codable, Hashable, Sendable {
/// Either a text document or a directory. A directory entry must end with a
/// forward slash "/" and a directory entry implies that every nested text
/// document within the directory belongs to this source item.
public var uri: URI
/// Type of file of the source item, such as whether it is file or directory.
public var kind: SourceItemKind
/// Indicates if this source is automatically generated by the build and is
/// not intended to be manually edited by the user.
public var generated: Bool
/// Kind of data to expect in the `data` field. If this field is not set, the kind of data is not specified.
public var dataKind: SourceItemDataKind?
/// Language-specific metadata about this source item.
public var data: LSPAny?
/// If `dataKind` is `sourceKit`, the `data` interpreted as `SourceKitSourceItemData`, otherwise `nil`.
public var sourceKitData: SourceKitSourceItemData? {
guard dataKind == .sourceKit else {
return nil
}
return SourceKitSourceItemData(fromLSPAny: data)
}
public init(
uri: URI,
kind: SourceItemKind,
generated: Bool,
dataKind: SourceItemDataKind? = nil,
data: LSPAny? = nil
) {
self.uri = uri
self.kind = kind
self.generated = generated
self.dataKind = dataKind
self.data = data
}
}
public enum SourceItemKind: Int, Codable, Hashable, Sendable {
/// The source item references a normal file.
case file = 1
/// The source item references a directory.
case directory = 2
}
public struct SourceItemDataKind: RawRepresentable, Codable, Hashable, Sendable {
public var rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
/// `data` field must contain a JvmSourceItemData object.
public static let jvm = SourceItemDataKind(rawValue: "jvm")
/// `data` field must contain a `SourceKitSourceItemData` object.
///
/// **(BSP Extension)**
public static let sourceKit = SourceItemDataKind(rawValue: "sourceKit")
}
/// **(BSP Extension)**
public enum SourceKitSourceItemKind: String, Codable {
/// A source file that belongs to the target
case source = "source"
/// A header file that is clearly associated with one target.
///
/// For example header files in SwiftPM projects are always associated to one target and SwiftPM can provide build
/// settings for that header file.
///
/// In general, build servers don't need to list all header files in the `buildTarget/sources` request: Semantic
/// functionality for header files is usually provided by finding a main file that includes the header file and
/// inferring build settings from it. Listing header files in `buildTarget/sources` allows SourceKit-LSP to provide
/// semantic functionality for header files if they haven't been included by any main file.
case header = "header"
/// A SwiftDocC documentation catalog usually ending in the ".docc" extension.
case doccCatalog = "doccCatalog"
}
public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
/// The language of the source file. If `nil`, the language is inferred from the file extension.
public var language: Language?
/// The kind of source file that this source item represents. If omitted, the item is assumed to be a normal source file,
/// ie. omitting this key is equivalent to specifying it as `source`.
public var kind: SourceKitSourceItemKind?
/// The output path that is used during indexing for this file, ie. the `-index-unit-output-path`, if it is specified
/// in the compiler arguments or the file that is passed as `-o`, if `-index-unit-output-path` is not specified.
///
/// This allows SourceKit-LSP to remove index entries for source files that are removed from a target but remain
/// present on disk.
///
/// The server communicates during the initialize handshake whether it populates this property by setting
/// `outputPathsProvider: true` in `SourceKitInitializeBuildResponseData`.
public var outputPath: String?
/// If this source item gets copied to a different destination during preparation, the destinations it will be copied
/// to.
///
/// If a user action would jump to one of these copied files, this allows SourceKit-LSP to redirect the navigation to
/// the original source file instead of jumping to a file in the build directory.
public var copyDestinations: [DocumentURI]?
public init(
language: Language? = nil,
kind: SourceKitSourceItemKind? = nil,
outputPath: String? = nil,
copyDestinations: [DocumentURI]? = nil
) {
self.language = language
self.kind = kind
self.outputPath = outputPath
self.copyDestinations = copyDestinations
}
public init?(fromLSPDictionary dictionary: [String: LanguageServerProtocol.LSPAny]) {
if case .string(let language) = dictionary[CodingKeys.language.stringValue] {
self.language = Language(rawValue: language)
}
if case .string(let rawKind) = dictionary[CodingKeys.kind.stringValue] {
self.kind = SourceKitSourceItemKind(rawValue: rawKind)
}
// Backwards compatibility for isHeader
if case .bool(let isHeader) = dictionary["isHeader"], isHeader {
self.kind = .header
}
if case .string(let outputFilePath) = dictionary[CodingKeys.outputPath.stringValue] {
self.outputPath = outputFilePath
}
if case .array(let copyDestinations) = dictionary[CodingKeys.copyDestinations.stringValue] {
self.copyDestinations = copyDestinations.compactMap { entry in
guard case .string(let copyDestination) = entry else {
return nil
}
return try? DocumentURI(string: copyDestination)
}
}
}
public func encodeToLSPAny() -> LanguageServerProtocol.LSPAny {
var result: [String: LSPAny] = [:]
if let language {
result[CodingKeys.language.stringValue] = .string(language.rawValue)
}
if let kind {
result[CodingKeys.kind.stringValue] = .string(kind.rawValue)
}
if let outputPath {
result[CodingKeys.outputPath.stringValue] = .string(outputPath)
}
if let copyDestinations {
result[CodingKeys.copyDestinations.stringValue] = .array(copyDestinations.map { .string($0.stringValue) })
}
return .dictionary(result)
}
}