//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// import Foundation @_spi(SourceKitLSP) package import LanguageServerProtocol @_spi(SourceKitLSP) import SKLogging import SKUtilities import SourceKitLSP import SwiftExtensions import SwiftSyntax package struct InlayHintResolveData: Codable, LSPAnyCodable { package let uri: DocumentURI package let position: Position package let version: Int package init(uri: DocumentURI, position: Position, version: Int) { self.uri = uri self.position = position self.version = version } } private struct InlayHintCacheEntry { let version: Int /// Marks hints as position-shifted but semantically stale; trigger a background recompute before treating cache as fully fresh. let needsSemanticRefresh: Bool /// The cached hints, sorted ascending by their position in the document. let hints: [InlayHint] } private struct InFlightInlayHintRefreshTask { let id: UUID let expectedVersion: Int let task: Task<(), any Error> var sendRefreshRequest: Bool } actor InlayHintManager { /// Cached inlay hints for each document. /// /// Each entry stores hints for the full document and the document version they were computed for. /// - Note: The capacity has been chosen without scientific measurements. 20 seems like a resonable number of open documents a client may have. private var cache = LRUCache(capacity: 20) /// Documents that currently have a background inlay-hint recomputation in progress. /// /// Used to avoid scheduling multiple concurrent recomputations for the same document. private var inFlightRefreshTasks: [DocumentURI: InFlightInlayHintRefreshTask] = [:] func getCachedInlayHints( swiftLanguageService service: SwiftLanguageService, for snapshot: DocumentSnapshot, range: Range? ) -> [InlayHint]? { guard let entry = cache[snapshot.uri] else { scheduleInlayHintRefresh(swiftLanguageService: service, for: snapshot) return nil } if entry.version != snapshot.version || entry.needsSemanticRefresh { // The cached hints are stale. Schedule a refresh and return the stale hints for now. The stale hints are still in the correct position they may just contain outdated type information. scheduleInlayHintRefresh(swiftLanguageService: service, for: snapshot) } return filterInlayHints(entry.hints, in: range) } private func filterInlayHints(_ hints: [InlayHint], in range: Range?) -> [InlayHint] { guard let range else { return hints } let lowerBoundIndex = hints.binarySearchFirst(where: { $0.position >= range.lowerBound }) let upperBoundIndex = hints.binarySearchFirst(where: { $0.position >= range.upperBound }) return Array(hints[lowerBoundIndex.. Position { let newUtf16Index = if position.line == range.upperBound.line { if lineDelta > 0 { // The line does change, we thus have to calculate the offset of the hint in the new line // This offset has the length of all characters added by the edit after the newline (columnDelta) // + the length of the text between the end of the edit and the start of the hint columnDelta + position.utf16index - range.upperBound.utf16index } else { // The line does not change, just add the column delta position.utf16index + columnDelta } } else { // The hint is on a different line than the edit, the column doesn't change position.utf16index } return Position(line: position.line + lineDelta, utf16index: newUtf16Index) } let previousHints = currentHints currentHints = [] let lowerBoundIndex = previousHints.binarySearchFirst(where: { $0.position >= range.lowerBound }) let upperBoundIndex = previousHints.binarySearchFirst(where: { $0.position >= range.upperBound }) // Hints before the edit range are unaffected. currentHints.append(contentsOf: previousHints[..= snapshot.version { // We already have a task running for a newer version of the document, so we don't need to schedule another one. // But we may have to update the `sendRefreshRequest` property, if the already running task did not need a refresh but the new one does inFlightTask.sendRefreshRequest = inFlightTask.sendRefreshRequest || sendRefreshRequest inFlightRefreshTasks[uri] = inFlightTask return } // Cancel the currently running task for the older version of the document, since we will schedule a new one for the newer version below. inFlightTask.task.cancel() } let taskID = UUID() let task = Task(priority: .medium) { [self, service] in try await run { do { try Task.checkCancellation() // We recompute inlay hints for the whole document, even if only a range was requested. This is because edits // can affect the validity of inlay hints outside of their edit range (e.g., an edit that changes a variable's // type can make type hints for all other variables that use the edited variable stale). Caching and returning // inlay hints for only a subrange of the document would add a lot of complexity, because we would need to track // which hints are valid for which ranges and versions. let updatedHints = try await computeTypeInlayHints(swiftLanguageService: service, for: snapshot, range: nil) try Task.checkCancellation() let updatedEntry = InlayHintCacheEntry( version: snapshot.version, needsSemanticRefresh: false, hints: updatedHints ) cache[uri] = updatedEntry guard let inFlightTask = inFlightRefreshTasks[uri], inFlightTask.id == taskID else { return } if inFlightTask.sendRefreshRequest { let _ = try await service.sourceKitLSPServer?.sendRequestToClient(InlayHintRefreshRequest()) } } catch is CancellationError { return } catch { logger.error("Inlay hint refresh failed for \(uri.forLogging): \(error.forLogging)") } } cleanup: { guard let inFlightTask = inFlightRefreshTasks[uri], inFlightTask.id == taskID else { // This task was already replaced by another one so we should not remove the entry return } inFlightRefreshTasks[uri] = nil } } inFlightRefreshTasks[uri] = InFlightInlayHintRefreshTask( id: taskID, expectedVersion: snapshot.version, task: task, sendRefreshRequest: sendRefreshRequest ) } func computeTypeInlayHints( swiftLanguageService service: SwiftLanguageService, for snapshot: DocumentSnapshot, range: Range? ) async throws -> [InlayHint] { let infos = try await service.variableTypeInfos(snapshot.uri, nil) return infos .lazy .filter { !$0.hasExplicitType } .map { info -> InlayHint in let position = info.range.upperBound let variableStart = info.range.lowerBound let label = ": \(info.printedType)" let textEdits: [TextEdit]? if info.canBeFollowedByTypeAnnotation { textEdits = [TextEdit(range: position.. Bool) -> Int { var low = 0 var high = self.count while low < high { let mid = (low + high) / 2 if predicate(self[mid]) { high = mid } else { low = mid + 1 } } return low } }