mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
347 lines
13 KiB
Swift
347 lines
13 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import LanguageServerProtocol
|
|
import BuildServerProtocol
|
|
import LSPLogging
|
|
import Dispatch
|
|
|
|
import struct TSCBasic.AbsolutePath
|
|
|
|
/// Status for a given main file.
|
|
enum MainFileStatus: Equatable {
|
|
/// Waiting for the `BuildSystem` to return settings.
|
|
case waiting
|
|
|
|
/// No response from `BuildSystem` yet, using arguments from the fallback build system.
|
|
case waitingUsingFallback(FileBuildSettings)
|
|
|
|
/// Two cases here:
|
|
/// - Primary build system gave us fallback arguments to use.
|
|
/// - Primary build system didn't handle the file, using arguments from the fallback build system.
|
|
/// No longer waiting.
|
|
case fallback(FileBuildSettings)
|
|
|
|
/// Using settings from the primary `BuildSystem`.
|
|
case primary(FileBuildSettings)
|
|
|
|
/// No settings provided by primary and fallback `BuildSystem`s.
|
|
case unsupported
|
|
}
|
|
|
|
extension MainFileStatus {
|
|
/// Whether fallback build settings are being used.
|
|
/// If no build settings are available, returns false.
|
|
var usingFallbackSettings: Bool {
|
|
switch self {
|
|
case .waiting: return false
|
|
case .unsupported: return false
|
|
case .waitingUsingFallback(_): return true
|
|
case .fallback(_): return true
|
|
case .primary(_): return false
|
|
}
|
|
}
|
|
|
|
/// The active build settings, if any.
|
|
var buildSettings: FileBuildSettings? {
|
|
switch self {
|
|
case .waiting: return nil
|
|
case .unsupported: return nil
|
|
case .waitingUsingFallback(let settings): return settings
|
|
case .fallback(let settings): return settings
|
|
case .primary(let settings): return settings
|
|
}
|
|
}
|
|
|
|
/// Corresponding change from this status, if any.
|
|
var buildSettingsChange: FileBuildSettingsChange? {
|
|
switch self {
|
|
case .waiting: return nil
|
|
case .unsupported: return .removedOrUnavailable
|
|
case .waitingUsingFallback(let settings): return .fallback(settings)
|
|
case .fallback(let settings): return .fallback(settings)
|
|
case .primary(let settings): return .modified(settings)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `BuildSystem` that integrates client-side information such as main-file lookup as well as providing
|
|
/// common functionality such as caching.
|
|
///
|
|
/// This `BuildSystem` combines settings from optional primary and fallback
|
|
/// build systems. We assume the fallback system does not integrate with change
|
|
/// notifications; at the moment the fallback must be a `FallbackBuildSystem` if
|
|
/// present.
|
|
///
|
|
/// Since some `BuildSystem`s may require a bit of a time to compute their arguments asynchronously,
|
|
/// this class has a configurable `buildSettings` timeout which denotes the amount of time to give
|
|
/// the build system before applying the fallback arguments.
|
|
public actor BuildSystemManager {
|
|
/// The set of watched files, along with their main file and language.
|
|
var watchedFiles: [DocumentURI: (mainFile: DocumentURI, language: Language)] = [:]
|
|
|
|
/// The underlying primary build system.
|
|
let buildSystem: BuildSystem?
|
|
|
|
/// Timeout before fallback build settings are used.
|
|
let fallbackSettingsTimeout: DispatchTimeInterval
|
|
|
|
/// The fallback build system. If present, used when the `buildSystem` is not
|
|
/// set or cannot provide settings.
|
|
let fallbackBuildSystem: FallbackBuildSystem?
|
|
|
|
/// Provider of file to main file mappings.
|
|
var _mainFilesProvider: MainFilesProvider?
|
|
|
|
/// Build system delegate that will receive notifications about setting changes, etc.
|
|
var _delegate: BuildSystemDelegate?
|
|
|
|
/// Create a BuildSystemManager that wraps the given build system. The new
|
|
/// manager will modify the delegate of the underlying build system.
|
|
public init(buildSystem: BuildSystem?, fallbackBuildSystem: FallbackBuildSystem?,
|
|
mainFilesProvider: MainFilesProvider?, fallbackSettingsTimeout: DispatchTimeInterval = .seconds(3)) async {
|
|
let buildSystemHasDelegate = await buildSystem?.delegate != nil
|
|
precondition(!buildSystemHasDelegate)
|
|
self.buildSystem = buildSystem
|
|
self.fallbackBuildSystem = fallbackBuildSystem
|
|
self._mainFilesProvider = mainFilesProvider
|
|
self.fallbackSettingsTimeout = fallbackSettingsTimeout
|
|
await self.buildSystem?.setDelegate(self)
|
|
}
|
|
|
|
public func filesDidChange(_ events: [FileEvent]) async {
|
|
await self.buildSystem?.filesDidChange(events)
|
|
}
|
|
}
|
|
|
|
extension BuildSystemManager {
|
|
public var delegate: BuildSystemDelegate? {
|
|
get { _delegate }
|
|
set { _delegate = newValue }
|
|
}
|
|
|
|
/// - Note: Needed so we can set the delegate from a different isolation context.
|
|
public func setDelegate(_ delegate: BuildSystemDelegate?) {
|
|
self.delegate = delegate
|
|
}
|
|
|
|
public var mainFilesProvider: MainFilesProvider? {
|
|
get { _mainFilesProvider}
|
|
set { _mainFilesProvider = newValue }
|
|
}
|
|
|
|
/// - Note: Needed so we can set the delegate from a different isolation context.
|
|
public func setMainFilesProvider(_ mainFilesProvider: MainFilesProvider?) {
|
|
self.mainFilesProvider = mainFilesProvider
|
|
}
|
|
|
|
private func buildSettings(
|
|
for document: DocumentURI,
|
|
language: Language
|
|
) async -> (buildSettings: FileBuildSettings, isFallback: Bool)? {
|
|
do {
|
|
// FIXME: (async) We should only wait `fallbackSettingsTimeout` for build
|
|
// settings and return fallback afterwards. I am not sure yet, how best to
|
|
// implement that with Swift concurrency.
|
|
// For now, this should be fine because all build systems return
|
|
// very quickly from `settings(for:language:)`.
|
|
if let settings = try await buildSystem?.buildSettings(for: document, language: language) {
|
|
return (buildSettings: settings, isFallback: false)
|
|
}
|
|
} catch {
|
|
log("Getting build settings failed: \(error)")
|
|
}
|
|
if let settings = fallbackBuildSystem?.buildSettings(for: document, language: language) {
|
|
// If there is no build system and we only have the fallback build system,
|
|
// we will never get real build settings. Consider the build settings
|
|
// non-fallback.
|
|
return (buildSettings: settings, isFallback: buildSystem != nil)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Returns the build settings for the given document.
|
|
///
|
|
/// If the document doesn't have builds settings by itself, eg. because it is
|
|
/// a C header file, the build settings will be inferred from the primary main
|
|
/// file of the document. In practice this means that we will compute the build
|
|
/// settings of a C file that includes the header and replace any file
|
|
/// references to that C file in the build settings by the header file.
|
|
public func buildSettingsInferredFromMainFile(
|
|
for document: DocumentURI,
|
|
language: Language
|
|
) async -> (buildSettings: FileBuildSettings, isFallback: Bool)? {
|
|
if let mainFile = mainFilesProvider?.mainFilesContainingFile(document).first {
|
|
if let mainFileBuildSettings = await buildSettings(for: mainFile, language: language) {
|
|
return (
|
|
buildSettings: mainFileBuildSettings.buildSettings.patching(newFile: document.pseudoPath, originalFile: mainFile.pseudoPath),
|
|
isFallback: mainFileBuildSettings.isFallback
|
|
)
|
|
}
|
|
}
|
|
return await buildSettings(for: document, language: language)
|
|
}
|
|
|
|
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
|
|
log("registerForChangeNotifications(\(uri.pseudoPath))")
|
|
let mainFile: DocumentURI
|
|
|
|
if let watchedFile = self.watchedFiles[uri] {
|
|
mainFile = watchedFile.mainFile
|
|
} else {
|
|
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri)
|
|
mainFile = chooseMainFile(for: uri, from: mainFiles ?? [])
|
|
self.watchedFiles[uri] = (mainFile, language)
|
|
}
|
|
|
|
await buildSystem?.registerForChangeNotifications(for: mainFile, language: language)
|
|
}
|
|
|
|
/// Return settings for `file` based on the `change` settings for `mainFile`.
|
|
///
|
|
/// This is used when inferring arguments for header files (e.g. main file is a `.m` while file is a` .h`).
|
|
nonisolated func convert(
|
|
change: FileBuildSettingsChange,
|
|
ofMainFile mainFile: DocumentURI,
|
|
to file: DocumentURI
|
|
) -> FileBuildSettingsChange {
|
|
guard mainFile != file else { return change }
|
|
switch change {
|
|
case .removedOrUnavailable: return .removedOrUnavailable
|
|
case .fallback(let settings):
|
|
return .fallback(settings.patching(newFile: file.pseudoPath, originalFile: mainFile.pseudoPath))
|
|
case .modified(let settings):
|
|
return .modified(settings.patching(newFile: file.pseudoPath, originalFile: mainFile.pseudoPath))
|
|
}
|
|
}
|
|
|
|
public func unregisterForChangeNotifications(for uri: DocumentURI) async {
|
|
guard let mainFile = self.watchedFiles[uri]?.mainFile else {
|
|
log("Unbalanced calls for registerForChangeNotifications and unregisterForChangeNotifications", level: .warning)
|
|
return
|
|
}
|
|
self.watchedFiles[uri] = nil
|
|
await self.checkUnreferencedMainFile(mainFile)
|
|
}
|
|
|
|
/// If the given main file is no longer referenced by any watched files,
|
|
/// remove it and unregister it at the underlying build system.
|
|
func checkUnreferencedMainFile(_ mainFile: DocumentURI) async {
|
|
if !self.watchedFiles.values.lazy.map({ $0.mainFile }).contains(mainFile) {
|
|
// This was the last reference to the main file. Remove it.
|
|
await self.buildSystem?.unregisterForChangeNotifications(for: mainFile)
|
|
}
|
|
}
|
|
|
|
public func fileHandlingCapability(for uri: DocumentURI) async -> FileHandlingCapability {
|
|
return max(
|
|
await buildSystem?.fileHandlingCapability(for: uri) ?? .unhandled,
|
|
fallbackBuildSystem != nil ? .fallback : .unhandled
|
|
)
|
|
}
|
|
}
|
|
|
|
extension BuildSystemManager: BuildSystemDelegate {
|
|
public func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async {
|
|
let changedWatchedFiles = changedFiles.flatMap({ mainFile in
|
|
self.watchedFiles.filter { $1.mainFile == mainFile }.keys
|
|
})
|
|
|
|
if !changedWatchedFiles.isEmpty, let delegate = self._delegate {
|
|
await delegate.fileBuildSettingsChanged(Set(changedWatchedFiles))
|
|
}
|
|
}
|
|
|
|
public func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) async {
|
|
// Empty changes --> assume everything has changed.
|
|
guard !changedFiles.isEmpty else {
|
|
if let delegate = self._delegate {
|
|
await delegate.filesDependenciesUpdated(changedFiles)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Need to map the changed main files back into changed watch files.
|
|
let changedWatchedFiles = self.watchedFiles.filter { changedFiles.contains($1.mainFile) }
|
|
let newChangedFiles = Set(changedWatchedFiles.map { $0.key })
|
|
if let delegate = self._delegate, !newChangedFiles.isEmpty {
|
|
await delegate.filesDependenciesUpdated(newChangedFiles)
|
|
}
|
|
}
|
|
|
|
public func buildTargetsChanged(_ changes: [BuildTargetEvent]) async {
|
|
if let delegate = self._delegate {
|
|
await delegate.buildTargetsChanged(changes)
|
|
}
|
|
}
|
|
|
|
public func fileHandlingCapabilityChanged() async {
|
|
if let delegate = self._delegate {
|
|
await delegate.fileHandlingCapabilityChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension BuildSystemManager: MainFilesDelegate {
|
|
// FIXME: Consider debouncing/limiting this, seems to trigger often during a build.
|
|
public func mainFilesChanged() async {
|
|
let origWatched = self.watchedFiles
|
|
self.watchedFiles = [:]
|
|
var buildSettingsChanges = Set<DocumentURI>()
|
|
|
|
for (uri, state) in origWatched {
|
|
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? []
|
|
let newMainFile = chooseMainFile(for: uri, previous: state.mainFile, from: mainFiles)
|
|
let language = state.language
|
|
|
|
self.watchedFiles[uri] = (newMainFile, language)
|
|
|
|
if state.mainFile != newMainFile {
|
|
log("main file for '\(uri)' changed old: '\(state.mainFile)' -> new: '\(newMainFile)'", level: .info)
|
|
await self.checkUnreferencedMainFile(state.mainFile)
|
|
|
|
buildSettingsChanges.insert(uri)
|
|
}
|
|
}
|
|
|
|
if let delegate = self._delegate, !buildSettingsChanges.isEmpty {
|
|
await delegate.fileBuildSettingsChanged(buildSettingsChanges)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension BuildSystemManager {
|
|
|
|
/// *For Testing* Returns the main file used for `uri`, if this is a registered file.
|
|
public func _cachedMainFile(for uri: DocumentURI) -> DocumentURI? {
|
|
watchedFiles[uri]?.mainFile
|
|
}
|
|
}
|
|
|
|
/// Choose a new main file for the given uri, preferring to use a previous main file if still
|
|
/// available, to avoid thrashing the settings unnecessarily, and falling back to `uri` itself if
|
|
/// there are no main files found at all.
|
|
private func chooseMainFile(
|
|
for uri: DocumentURI,
|
|
previous: DocumentURI? = nil,
|
|
from mainFiles: Set<DocumentURI>) -> DocumentURI
|
|
{
|
|
if let previous = previous, mainFiles.contains(previous) {
|
|
return previous
|
|
} else if mainFiles.isEmpty || mainFiles.contains(uri) {
|
|
return uri
|
|
} else {
|
|
return mainFiles.first!
|
|
}
|
|
}
|