From d7837cdc074ddba7b139acb8de646a12f23aeb19 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 27 Dec 2024 08:18:35 -0500 Subject: [PATCH] Removing priority set concept, new tests for bug fixes --- .../Neon/TextSystemInterface+Validation.swift | 2 +- Sources/Neon/TextSystemInterface.swift | 3 - Sources/Neon/TextSystemStyler.swift | 30 +---- Sources/Neon/TextViewHighlighter.swift | 4 +- Sources/Neon/TextViewSystemInterface.swift | 38 +----- Sources/Neon/ThreePhaseTextSystemStyler.swift | 16 +-- Sources/Neon/TreeSitterClient+Neon.swift | 2 +- Sources/RangeState/AwaitableQueue.swift | 71 ++++++++++ .../HybridSyncAsyncValueProvider.swift | 8 +- .../HybridValueProvider+RangeProcessor.swift | 8 +- Sources/RangeState/RangeProcessor.swift | 64 ++------- Sources/RangeState/RangeValidator.swift | 16 +-- .../SinglePhaseRangeValidator.swift | 121 +++++++++--------- .../RangeState/ThreePhaseRangeValidator.swift | 46 +++---- .../BackgroundingLanguageLayerTree.swift | 2 + .../HybridSyncAsyncVersionedResource.swift | 4 +- .../TreeSitterClient/TreeSitterClient.swift | 43 +++++-- .../RangeStateTests/RangeProcessorTests.swift | 24 ++++ .../SinglePhaseRangeValidatorTests.swift | 82 ++++++++++++ 19 files changed, 340 insertions(+), 244 deletions(-) create mode 100644 Sources/RangeState/AwaitableQueue.swift diff --git a/Sources/Neon/TextSystemInterface+Validation.swift b/Sources/Neon/TextSystemInterface+Validation.swift index 92755d9..e8b3f07 100644 --- a/Sources/Neon/TextSystemInterface+Validation.swift +++ b/Sources/Neon/TextSystemInterface+Validation.swift @@ -46,7 +46,7 @@ extension TextSystemInterface { mainActorAsyncValue: { contentRange in await asyncValidate( contentRange, - provider: { range in await provider.async(range) } + provider: { range in await provider.async(isolation: MainActor.shared, range) } ) } ) diff --git a/Sources/Neon/TextSystemInterface.swift b/Sources/Neon/TextSystemInterface.swift index c35c122..a6e22c8 100644 --- a/Sources/Neon/TextSystemInterface.swift +++ b/Sources/Neon/TextSystemInterface.swift @@ -8,9 +8,6 @@ public protocol TextSystemInterface { @MainActor func applyStyles(for application: TokenApplication) - @MainActor - var visibleSet: IndexSet { get } - @MainActor var content: Content { get } } diff --git a/Sources/Neon/TextSystemStyler.swift b/Sources/Neon/TextSystemStyler.swift index 73d1973..4bb2dc3 100644 --- a/Sources/Neon/TextSystemStyler.swift +++ b/Sources/Neon/TextSystemStyler.swift @@ -6,7 +6,7 @@ import RangeState /// /// This is the main component that coordinates the styling and invalidation of text. It interfaces with the text system via `TextSystemInterface`. Actual token information is provided from a `TokenProvider`. /// -/// The `TextSystemInterface` is what to update, but it is up to you to tell it when that updating is needed. This is done via the `invalidate(_:)` call, as well as `visibleContentDidChange()`. It will be also be done automatically when the content changes. +/// The `TextSystemInterface` is what to update, but it is up to you to tell it when that updating is needed. This is done via the `invalidate(_:)` call, as well as `validate(_:)`. It will be also be done automatically when the content changes. /// /// > Note: A `TextSystemStyler` must be informed of all text content changes made using `didChangeContent(in:, delta:)`. @MainActor @@ -27,10 +27,8 @@ public final class TextSystemStyler { self.validator = SinglePhaseRangeValidator( configuration: .init( versionedContent: textSystem.content, - provider: tokenValidator.validationProvider, - prioritySetProvider: { textSystem.visibleSet } - ), - isolation: MainActor.shared + provider: tokenValidator.validationProvider + ) ) } @@ -52,29 +50,11 @@ public final class TextSystemStyler { validator.contentChanged(in: range, delta: delta) } - /// Calculates any newly-visible text that is invalid - /// - /// You should invoke this method when the visible text in your system changes. - public func visibleContentDidChange() { - let prioritySet = textSystem.visibleSet - - validator.validate(.set(prioritySet), prioritizing: prioritySet, isolation: MainActor.shared) - } - - public func invalidate(_ target: RangeTarget) { validator.invalidate(target) } - public func validate(_ target: RangeTarget) { - let prioritySet = textSystem.visibleSet - - validator.validate(target, prioritizing: prioritySet, isolation: MainActor.shared) - } - - public func validate() { - let prioritySet = textSystem.visibleSet - - validator.validate(.set(prioritySet), prioritizing: prioritySet, isolation: MainActor.shared) + public func validate(_ target: RangeTarget = .all) { + validator.validate(target) } } diff --git a/Sources/Neon/TextViewHighlighter.swift b/Sources/Neon/TextViewHighlighter.swift index fe32c62..4122105 100644 --- a/Sources/Neon/TextViewHighlighter.swift +++ b/Sources/Neon/TextViewHighlighter.swift @@ -192,7 +192,9 @@ extension TextViewHighlighter { } @objc private func visibleContentChanged(_ notification: NSNotification) { - styler.visibleContentDidChange() + let visibleRange = textView.visibleTextRange + + styler.validate(.range(visibleRange)) } } diff --git a/Sources/Neon/TextViewSystemInterface.swift b/Sources/Neon/TextViewSystemInterface.swift index ad9f9bd..5a0aecf 100644 --- a/Sources/Neon/TextViewSystemInterface.swift +++ b/Sources/Neon/TextViewSystemInterface.swift @@ -57,14 +57,11 @@ public struct TextViewSystemInterface { extension TextViewSystemInterface: TextSystemInterface { private var effectiveInterface: (any TextSystemInterface)? { - let provider = { visibleSet } - if #available(macOS 12.0, iOS 16.0, tvOS 16.0, *) { if let textLayoutManager { return TextLayoutManagerSystemInterface( textLayoutManager: textLayoutManager, - attributeProvider: attributeProvider, - visibleSetProvider: provider + attributeProvider: attributeProvider ) } } @@ -73,8 +70,7 @@ extension TextViewSystemInterface: TextSystemInterface { if let layoutManager { return LayoutManagerSystemInterface( layoutManager: layoutManager, - attributeProvider: attributeProvider, - visibleSetProvider: provider + attributeProvider: attributeProvider ) } #endif @@ -89,10 +85,6 @@ extension TextViewSystemInterface: TextSystemInterface { effectiveInterface?.applyStyles(for: application) } - public var visibleSet: IndexSet { - IndexSet(integersIn: textView.visibleTextRange) - } - public var content: NSTextStorage { textStorage } @@ -106,19 +98,16 @@ extension TextViewSystemInterface: TextSystemInterface { public struct LayoutManagerSystemInterface { public let layoutManager: NSLayoutManager public let attributeProvider: TokenAttributeProvider - public let visibleSetProvider: () -> IndexSet private let placeholderStorage = NSTextStorage() - public init(layoutManager: NSLayoutManager, attributeProvider: @escaping TokenAttributeProvider, visibleSetProvider: @escaping () -> IndexSet) { + public init(layoutManager: NSLayoutManager, attributeProvider: @escaping TokenAttributeProvider) { self.layoutManager = layoutManager self.attributeProvider = attributeProvider - self.visibleSetProvider = visibleSetProvider } public init?(textView: TextView, attributeProvider: @escaping TokenAttributeProvider) { guard let layoutManager = textView.layoutManager else { return nil } self.layoutManager = layoutManager - self.visibleSetProvider = { IndexSet(integersIn: textView.visibleTextRange) } self.attributeProvider = attributeProvider } } @@ -141,10 +130,6 @@ extension LayoutManagerSystemInterface: TextSystemInterface { } } - public var visibleSet: IndexSet { - visibleSetProvider() - } - public var content: NSTextStorage { layoutManager.textStorage ?? placeholderStorage } @@ -158,19 +143,16 @@ extension LayoutManagerSystemInterface: TextSystemInterface { public struct TextLayoutManagerSystemInterface { public let textLayoutManager: NSTextLayoutManager public let attributeProvider: TokenAttributeProvider - public let visibleSetProvider: () -> IndexSet private let placholderContent = NSTextContentManager() - public init(textLayoutManager: NSTextLayoutManager, attributeProvider: @escaping TokenAttributeProvider, visibleSetProvider: @escaping () -> IndexSet) { + public init(textLayoutManager: NSTextLayoutManager, attributeProvider: @escaping TokenAttributeProvider) { self.textLayoutManager = textLayoutManager self.attributeProvider = attributeProvider - self.visibleSetProvider = visibleSetProvider } public init?(textView: TextView, attributeProvider: @escaping TokenAttributeProvider) { guard let textLayoutManager = textView.textLayoutManager else { return nil } self.textLayoutManager = textLayoutManager - self.visibleSetProvider = { IndexSet(integersIn: textView.visibleTextRange) } self.attributeProvider = attributeProvider } } @@ -205,10 +187,6 @@ extension TextLayoutManagerSystemInterface: TextSystemInterface { } } - public var visibleSet: IndexSet { - visibleSetProvider() - } - public var content: NSTextContentManager { contentManager } @@ -220,17 +198,14 @@ public struct TextStorageSystemInterface { private let textStorage: NSTextStorage public let attributeProvider: TokenAttributeProvider public let defaultAttributesProvider: () -> [NSAttributedString.Key : Any] - public let visibleSetProvider: () -> IndexSet public init( textStorage: NSTextStorage, attributeProvider: @escaping TokenAttributeProvider, - visibleSetProvider: @escaping () -> IndexSet, defaultAttributesProvider: @escaping () -> [NSAttributedString.Key : Any] ) { self.textStorage = textStorage self.attributeProvider = attributeProvider - self.visibleSetProvider = visibleSetProvider self.defaultAttributesProvider = defaultAttributesProvider } @@ -240,7 +215,6 @@ public struct TextStorageSystemInterface { #else self.textStorage = textView.textStorage #endif - self.visibleSetProvider = { IndexSet(integersIn: textView.visibleTextRange) } self.attributeProvider = attributeProvider self.defaultAttributesProvider = { textView.typingAttributes } } @@ -268,10 +242,6 @@ extension TextStorageSystemInterface: TextSystemInterface { textStorage.endEditing() } - public var visibleSet: IndexSet { - visibleSetProvider() - } - public var content: some VersionedContent { textStorage } diff --git a/Sources/Neon/ThreePhaseTextSystemStyler.swift b/Sources/Neon/ThreePhaseTextSystemStyler.swift index 655976c..e13e4af 100644 --- a/Sources/Neon/ThreePhaseTextSystemStyler.swift +++ b/Sources/Neon/ThreePhaseTextSystemStyler.swift @@ -29,8 +29,7 @@ public final class ThreePhaseTextSystemStyler { provider: tokenValidator.validationProvider, fallbackHandler: textSystem.validatorFallbackHandler(with: fallbackHandler), secondaryProvider: textSystem.validatorSecondaryHandler(with: secondaryValidationProvider), - secondaryValidationDelay: 3.0, - prioritySetProvider: { textSystem.visibleSet } + secondaryValidationDelay: 3.0 ), isolation: MainActor.shared ) @@ -44,15 +43,12 @@ public final class ThreePhaseTextSystemStyler { validator.invalidate(target) } - public func validate(_ target: RangeTarget) { - let prioritySet = textSystem.visibleSet - - validator.validate(target, prioritizing: prioritySet, isolation: MainActor.shared) + public func validate(_ target: RangeTarget = .all) { + validator.validate(target, isolation: MainActor.shared) } - public func validate() { - let prioritySet = textSystem.visibleSet - - validator.validate(.set(prioritySet), prioritizing: prioritySet, isolation: MainActor.shared) + public var name: String? { + get { validator.name } + set { validator.name = newValue } } } diff --git a/Sources/Neon/TreeSitterClient+Neon.swift b/Sources/Neon/TreeSitterClient+Neon.swift index c5743d3..2c987c7 100644 --- a/Sources/Neon/TreeSitterClient+Neon.swift +++ b/Sources/Neon/TreeSitterClient+Neon.swift @@ -47,7 +47,7 @@ extension TreeSitterClient { mainActorAsyncValue: { [highlightsProvider] range in do { let params = TreeSitterClient.ClientQueryParams(range: range, textProvider: provider) - let namedRanges = try await highlightsProvider.async(params) + let namedRanges = try await highlightsProvider.async(isolation: MainActor.shared, params) return TokenApplication(namedRanges: namedRanges, nameMap: nameMap, range: range) } catch { diff --git a/Sources/RangeState/AwaitableQueue.swift b/Sources/RangeState/AwaitableQueue.swift new file mode 100644 index 0000000..0715326 --- /dev/null +++ b/Sources/RangeState/AwaitableQueue.swift @@ -0,0 +1,71 @@ +struct AwaitableQueue { + private typealias Continuation = CheckedContinuation + + private enum Event { + case element(Element) + case waiter(Continuation) + } + + private var pendingEvents = [Event]() + + init() { + + } + + public var hasPendingEvents: Bool { + pendingEvents.contains { event in + switch event { + case .element: + true + case .waiter: + false + } + } + } + + public mutating func processingCompleted(isolation: isolated any Actor) async { + if hasPendingEvents == false { + return + } + + await withCheckedContinuation { continuation in + self.pendingEvents.append(.waiter(continuation)) + } + } + + public mutating func enqueue(_ element: Element) { + self.pendingEvents.append(.element(element)) + } + + public var pendingElements: [Element] { + pendingEvents.compactMap { + switch $0 { + case let .element(value): + value + case .waiter: + nil + } + } + } + + public mutating func handlePendingWaiters() { + while let event = pendingEvents.first { + guard case let .waiter(continuation) = event else { break } + + continuation.resume() + pendingEvents.removeFirst() + } + } + + mutating func next() -> Element? { + handlePendingWaiters() + + guard case let .element(first) = pendingEvents.first else { + return nil + } + + self.pendingEvents.removeFirst() + + return first + } +} diff --git a/Sources/RangeState/HybridSyncAsyncValueProvider.swift b/Sources/RangeState/HybridSyncAsyncValueProvider.swift index 99569d6..92c22d9 100644 --- a/Sources/RangeState/HybridSyncAsyncValueProvider.swift +++ b/Sources/RangeState/HybridSyncAsyncValueProvider.swift @@ -3,7 +3,7 @@ import Foundation /// A type that can perform work both synchronously and asynchronously. public struct HybridSyncAsyncValueProvider { public typealias SyncValueProvider = (Input) throws(Failure) -> Output? - public typealias AsyncValueProvider = (isolated (any Actor)?, sending Input) async throws(Failure) -> sending Output + public typealias AsyncValueProvider = (isolated (any Actor), sending Input) async throws(Failure) -> sending Output public let syncValueProvider: SyncValueProvider public let asyncValueProvider: AsyncValueProvider @@ -16,10 +16,14 @@ public struct HybridSyncAsyncValueProvider { self.asyncValueProvider = asyncValue } - public func async(isolation: isolated (any Actor)? = #isolation, _ input: sending Input) async throws(Failure) -> sending Output { + public func async(isolation: isolated (any Actor), _ input: sending Input) async throws(Failure) -> sending Output { try await asyncValueProvider(isolation, input) } + @MainActor + public func async(_ input: sending Input) async throws(Failure) -> sending Output { + try await asyncValueProvider(MainActor.shared, input) + } public func sync(_ input: Input) throws(Failure) -> Output? { try syncValueProvider(input) diff --git a/Sources/RangeState/HybridValueProvider+RangeProcessor.swift b/Sources/RangeState/HybridValueProvider+RangeProcessor.swift index c22c075..ca8130c 100644 --- a/Sources/RangeState/HybridValueProvider+RangeProcessor.swift +++ b/Sources/RangeState/HybridValueProvider+RangeProcessor.swift @@ -3,7 +3,7 @@ import Foundation extension HybridSyncAsyncValueProvider { /// Construct a `HybridSyncAsyncValueProvider` that will first attempt to process a location using a `RangeProcessor`. public init( - isolation: isolated (any Actor)? = #isolation, + isolation: isolated (any Actor), rangeProcessor: RangeProcessor, inputTransformer: @escaping (Input) -> (Int, RangeFillMode), syncValue: @escaping SyncValueProvider, @@ -20,11 +20,13 @@ extension HybridSyncAsyncValueProvider { return nil } - func _asyncVersion(isolation: isolated(any Actor)?, input: sending Input) async throws(Failure) -> sending Output { + func _asyncVersion(isolation: isolated (any Actor), input: sending Input) async throws(Failure) -> sending Output { let (location, fill) = inputTransformer(input) rangeProcessor.processLocation(isolation: isolation, location, mode: fill) - await rangeProcessor.processingCompleted() + print("start", input) + await rangeProcessor.processingCompleted(isolation: isolation) + print("end", input) return try await asyncValue(input) } diff --git a/Sources/RangeState/RangeProcessor.swift b/Sources/RangeState/RangeProcessor.swift index baaf80d..ead1e00 100644 --- a/Sources/RangeState/RangeProcessor.swift +++ b/Sources/RangeState/RangeProcessor.swift @@ -19,14 +19,8 @@ public enum RangeFillMode: Sendable, Hashable { /// A type that can perform on-demand processing of range-based data. public final class RangeProcessor { - private typealias Continuation = CheckedContinuation<(), Never> private typealias VersionedMutation = Versioned - private enum Event { - case change(VersionedMutation) - case waiter(Continuation) - } - /// Function to apply changes. /// /// These mutations can come from the content being modified or from operations that require lazy processing. The parameter's postApplyLimit property defines the maximum read location and needs to be respected to preserve the lazy access semantics. @@ -52,7 +46,7 @@ public final class RangeProcessor { } } - private var pendingEvents = [Event]() + private var pendingEventQueue = AwaitableQueue() public let configuration: Configuration @@ -147,38 +141,18 @@ extension RangeProcessor { } public var hasPendingChanges: Bool { - pendingEvents.contains { event in - switch event { - case .change: - true - case .waiter: - false - } - } + pendingEventQueue.hasPendingEvents } - public func processingCompleted(isolation: isolated (any Actor)? = #isolation) async { - if hasPendingChanges == false { - return - } - - await withCheckedContinuation { continuation in - self.pendingEvents.append(.waiter(continuation)) - } + public func processingCompleted(isolation: isolated (any Actor)) async { + await pendingEventQueue.processingCompleted(isolation: isolation) } /// Array of any mutations that are scheduled to be applied. /// /// You can use this property to transform Range, IndexSet, and RangeTarget values to match the current content. public var pendingMutations: [RangeMutation] { - pendingEvents.compactMap { - switch $0 { - case let .change(mutation): - mutation.value - case .waiter: - nil - } - } + pendingEventQueue.pendingElements.map { $0.value } } public func didChangeContent(_ mutation: RangeMutation) { @@ -219,7 +193,7 @@ extension RangeProcessor { } private func processMutation(_ mutation: RangeMutation, in isolation: isolated (any Actor)?) { - self.pendingEvents.append(.change(VersionedMutation(mutation, version: version))) + pendingEventQueue.enqueue(VersionedMutation(mutation, version: version)) self.version += 1 // this requires a very strange workaround to get the correct isolation inheritance from this changeHandler arrangement. I believe this is a bug. @@ -235,20 +209,13 @@ extension RangeProcessor { private func completeContentChanged(_ mutation: RangeMutation, in isolation: isolated (any Actor)?) { self.processedVersion += 1 - resumeLeadingContinuations() - - guard case let .change(first) = pendingEvents.first else { + guard let first = pendingEventQueue.next() else { preconditionFailure() } precondition(first.version == processedVersion, "changes must always be completed in order") precondition(first.value == mutation, "completed mutation does not match the expected value") - self.pendingEvents.removeFirst() - - // do this again, just in case there are any - resumeLeadingContinuations() - updateProcessedLocation(by: mutation.delta) scheduleFilling(in: isolation) @@ -262,7 +229,9 @@ extension RangeProcessor { self.targetProcessingLocation = min(targetProcessingLocation, contentLength) let mutation = fillMutationNeeded(for: targetProcessingLocation, mode: .optional) - guard let mutation else { return } + guard let mutation else { + return + } processMutation(mutation, in: isolation) } @@ -270,6 +239,10 @@ extension RangeProcessor { private func scheduleFilling(in isolation: isolated (any Actor)?) { Task { self.continueFillingIfNeeded(isolation: isolation) + + // it is very important to double check here, in case + // any waiters stuck in and we have no more work to do + self.pendingEventQueue.handlePendingWaiters() } } } @@ -284,13 +257,4 @@ extension RangeProcessor { self.maximumProcessedLocation = newMax } - - private func resumeLeadingContinuations() { - while let event = pendingEvents.first { - guard case let .waiter(continuation) = event else { break } - - continuation.resume() - pendingEvents.removeFirst() - } - } } diff --git a/Sources/RangeState/RangeValidator.swift b/Sources/RangeState/RangeValidator.swift index 83c3c51..3957cf7 100644 --- a/Sources/RangeState/RangeValidator.swift +++ b/Sources/RangeState/RangeValidator.swift @@ -51,10 +51,10 @@ public final class RangeValidator { /// Begin a validation pass. /// /// This must ultimately be paired with a matching call to `completeValidation(of:with:)`. - public func beginValidation(of target: RangeTarget, prioritizing set: IndexSet? = nil) -> Action { + public func beginValidation(of target: RangeTarget) -> Action { let set = target.indexSet(with: length) - guard let neededRange = nextNeededRange(in: set, prioritizing: set) else { return .none } + guard let neededRange = nextNeededRange(in: set) else { return .none } self.pendingSet.insert(range: neededRange) self.pendingRequests += 1 @@ -139,19 +139,15 @@ extension RangeValidator { extension RangeValidator { /// Computes the next contiguous invalid range - private func nextNeededRange(in set: IndexSet, prioritizing prioritySet: IndexSet?) -> NSRange? { - let prioritySet = prioritySet ?? fullSet - + private func nextNeededRange(in set: IndexSet) -> NSRange? { // the candidate set is: // - invalid - // - within our priority (or everything if we have none) // - not already pending - let workingInvalidSet = invalidSet + return invalidSet .intersection(set) - .intersection(prioritySet) .subtracting(pendingSet) - - return workingInvalidSet.nsRangeView.first + .nsRangeView + .first } } diff --git a/Sources/RangeState/SinglePhaseRangeValidator.swift b/Sources/RangeState/SinglePhaseRangeValidator.swift index f8e052f..37895c7 100644 --- a/Sources/RangeState/SinglePhaseRangeValidator.swift +++ b/Sources/RangeState/SinglePhaseRangeValidator.swift @@ -5,67 +5,36 @@ import Rearrange public final class SinglePhaseRangeValidator { public typealias ContentRange = RangeValidator.ContentRange public typealias Provider = HybridSyncAsyncValueProvider - public typealias PrioritySetProvider = () -> IndexSet - private typealias Sequence = AsyncStream + private struct ValidationOperation { + let contentRange: ContentRange + let target: RangeTarget + } public struct Configuration { public let versionedContent: Content public let provider: Provider - public let prioritySetProvider: PrioritySetProvider? public init( versionedContent: Content, - provider: Provider, - prioritySetProvider: PrioritySetProvider? = nil + provider: Provider ) { self.versionedContent = versionedContent self.provider = provider - self.prioritySetProvider = prioritySetProvider } } - private let continuation: Sequence.Continuation private let primaryValidator: RangeValidator + private var eventQueue: AwaitableQueue public let configuration: Configuration - public var validationHandler: (NSRange) -> Void = { _ in } - - public init(configuration: Configuration, isolation: isolated (any Actor)) { - self.configuration = configuration - self.primaryValidator = RangeValidator(content: configuration.versionedContent) - - let (stream, continuation) = Sequence.makeStream() + public var validationHandler: (NSRange, Bool) -> Void = { _, _ in } + public var name: String? - self.continuation = continuation - - Task { [weak self] in - _ = isolation - - for await versionedRange in stream { - await self?.validateRangeAsync(versionedRange, isolation: isolation) - } - } - } - - @MainActor public init(configuration: Configuration) { self.configuration = configuration self.primaryValidator = RangeValidator(content: configuration.versionedContent) - - let (stream, continuation) = Sequence.makeStream() - - self.continuation = continuation - - Task { [weak self] in - for await versionedRange in stream { - await self?.validateRangeAsync(versionedRange, isolation: MainActor.shared) - } - } - } - - deinit { - continuation.finish() + self.eventQueue = AwaitableQueue() } private var version: Content.Version { @@ -80,31 +49,32 @@ public final class SinglePhaseRangeValidator { @discardableResult public func validate( _ target: RangeTarget, - prioritizing set: IndexSet? = nil, isolation: isolated (any Actor) ) -> RangeValidator.Action { // capture this first, because we're about to start one let outstanding = primaryValidator.hasOutstandingValidations - let action = primaryValidator.beginValidation(of: target, prioritizing: set) + let action = primaryValidator.beginValidation(of: target) switch action { case .none: + eventQueue.handlePendingWaiters() return .none case let .needed(contentRange): + let operation = ValidationOperation(contentRange: contentRange, target: target) + // if we have an outstanding async operation going, force this to be async too if outstanding { - enqueueValidation(for: contentRange) + enqueueValidation(operation, isolation: isolation) return action } guard let validation = configuration.provider.sync(contentRange) else { - enqueueValidation(for: contentRange) - + enqueueValidation(operation, isolation: isolation) return action } - completePrimaryValidation(of: contentRange, with: validation, isolation: isolation) + completePrimaryValidation(of: operation, with: validation, isolation: isolation) return .none } @@ -113,32 +83,38 @@ public final class SinglePhaseRangeValidator { @MainActor @discardableResult public func validate( - _ target: RangeTarget, - prioritizing set: IndexSet? = nil + _ target: RangeTarget ) -> RangeValidator.Action { - validate(target, prioritizing: set, isolation: MainActor.shared) + validate(target, isolation: MainActor.shared) } - private func completePrimaryValidation(of contentRange: ContentRange, with validation: Validation, isolation: isolated (any Actor)) { - primaryValidator.completeValidation(of: contentRange, with: validation) + private func completePrimaryValidation(of operation: ValidationOperation, with validation: Validation, isolation: isolated (any Actor)) { + primaryValidator.completeValidation(of: operation.contentRange, with: validation) switch validation { case .stale: - Task { - _ = isolation - - if contentRange.version == self.version { + Task { + if operation.contentRange.version == self.version { print("version unchanged after stale results, stopping validation") return } - let prioritySet = self.configuration.prioritySetProvider?() ?? IndexSet(contentRange.value) + validate(operation.target, isolation: isolation) + } + case let .success(range): + let complete = primaryValidator.isValid(operation.target) - self.validate(.set(prioritySet), isolation: isolation) + validationHandler(range, complete) + // this only makes sense if the content has remained unchanged + if complete { + eventQueue.handlePendingWaiters() + return + } + + Task { + validate(operation.target, isolation: isolation) } - case let .success(range): - validationHandler(range) } } @@ -159,13 +135,30 @@ public final class SinglePhaseRangeValidator { primaryValidator.contentChanged(in: range, delta: delta) } - private func enqueueValidation(for contentRange: ContentRange) { - continuation.yield(contentRange) + private func enqueueValidation(_ operation: ValidationOperation, isolation: isolated any Actor) { + eventQueue.enqueue(operation) + + Task { + await self.validateRangeAsync(isolation: isolation) + } } - private func validateRangeAsync(_ contentRange: ContentRange, isolation: isolated (any Actor)) async { - let validation = await self.configuration.provider.async(contentRange) + private func validateRangeAsync(isolation: isolated any Actor) async { + let name = name ?? "" + + print("A name:", name) + guard let operation = eventQueue.next() else { + preconditionFailure("There must always be a next operation to process") + } + + print("B name:", name) + let validation = await self.configuration.provider.async(isolation: isolation, operation.contentRange) + + print("C name:", name) + completePrimaryValidation(of: operation, with: validation, isolation: isolation) + } - completePrimaryValidation(of: contentRange, with: validation, isolation: isolation) + public func validationCompleted(isolation: isolated any Actor) async { + await eventQueue.processingCompleted(isolation: isolation) } } diff --git a/Sources/RangeState/ThreePhaseRangeValidator.swift b/Sources/RangeState/ThreePhaseRangeValidator.swift index 3b22eb5..f3ae20f 100644 --- a/Sources/RangeState/ThreePhaseRangeValidator.swift +++ b/Sources/RangeState/ThreePhaseRangeValidator.swift @@ -21,22 +21,19 @@ public final class ThreePhaseRangeValidator { public let fallbackHandler: FallbackHandler? public let secondaryProvider: SecondaryValidationProvider? public let secondaryValidationDelay: TimeInterval - public let prioritySetProvider: PrimaryValidator.PrioritySetProvider? public init( versionedContent: Content, provider: Provider, fallbackHandler: FallbackHandler? = nil, secondaryProvider: SecondaryValidationProvider? = nil, - secondaryValidationDelay: TimeInterval = 2.0, - prioritySetProvider: PrimaryValidator.PrioritySetProvider? + secondaryValidationDelay: TimeInterval = 2.0 ) { self.versionedContent = versionedContent self.provider = provider self.fallbackHandler = fallbackHandler self.secondaryProvider = secondaryProvider self.secondaryValidationDelay = secondaryValidationDelay - self.prioritySetProvider = prioritySetProvider } } @@ -52,16 +49,14 @@ public final class ThreePhaseRangeValidator { self.primaryValidator = PrimaryValidator( configuration: .init( versionedContent: configuration.versionedContent, - provider: configuration.provider, - prioritySetProvider: configuration.prioritySetProvider - ), - isolation: isolation + provider: configuration.provider + ) ) self.fallbackValidator = InternalValidator(content: configuration.versionedContent) self.secondaryValidator = InternalValidator(content: configuration.versionedContent) - func _validationHandler(_ range: NSRange) { + func _validationHandler(_ range: NSRange, _ complete: Bool) { handlePrimaryValidation(of: range, isolation: isolation) } @@ -84,26 +79,26 @@ public final class ThreePhaseRangeValidator { secondaryValidator?.invalidate(target) } - public func validate(_ target: RangeTarget, prioritizing set: IndexSet? = nil, isolation: isolated (any Actor)) { - let action = primaryValidator.validate(target, prioritizing: set, isolation: isolation) + public func validate(_ target: RangeTarget, isolation: isolated (any Actor)) { + let action = primaryValidator.validate(target, isolation: isolation) switch action { case .none: - scheduleSecondaryValidation(of: target, prioritizing: set, isolation: isolation) + scheduleSecondaryValidation(of: target, isolation: isolation) case let .needed(contentRange): - fallbackValidate(contentRange.value, prioritizing: set) + fallbackValidate(contentRange.value) } } @MainActor - public func validate(_ target: RangeTarget, prioritizing set: IndexSet? = nil) { - validate(target, prioritizing: set, isolation: MainActor.shared) + public func validate(_ target: RangeTarget) { + validate(target, isolation: MainActor.shared) } - private func fallbackValidate(_ targetRange: NSRange, prioritizing set: IndexSet?) -> Void { + private func fallbackValidate(_ targetRange: NSRange) -> Void { guard let provider = configuration.fallbackHandler else { return } - let action = fallbackValidator.beginValidation(of: .range(targetRange), prioritizing: set) + let action = fallbackValidator.beginValidation(of: .range(targetRange)) switch action { case .none: @@ -135,20 +130,24 @@ public final class ThreePhaseRangeValidator { task?.cancel() } + + public var name: String? { + get { primaryValidator.name } + set { primaryValidator.name = newValue } + } } extension ThreePhaseRangeValidator { private func handlePrimaryValidation(of range: NSRange, isolation: isolated (any Actor)) { let target = RangeTarget.range(range) - let prioritySet = configuration.prioritySetProvider?() ?? IndexSet(range) fallbackValidator.invalidate(target) secondaryValidator?.invalidate(target) - scheduleSecondaryValidation(of: target, prioritizing: prioritySet, isolation: isolation) + scheduleSecondaryValidation(of: target, isolation: isolation) } - private func scheduleSecondaryValidation(of target: RangeTarget, prioritizing set: IndexSet?, isolation: isolated (any Actor)) { + private func scheduleSecondaryValidation(of target: RangeTarget, isolation: isolated (any Actor)) { if configuration.secondaryProvider == nil || secondaryValidator == nil { return } @@ -159,18 +158,15 @@ extension ThreePhaseRangeValidator { let delay = max(UInt64(configuration.secondaryValidationDelay * 1_000_000_000), 0) self.task = Task { - _ = isolation - try await Task.sleep(nanoseconds: delay) - await secondaryValidate(target: target, requestingVersion: requestingVersion, prioritizing: set, isolation: isolation) + await secondaryValidate(target: target, requestingVersion: requestingVersion, isolation: isolation) } } private func secondaryValidate( target: RangeTarget, requestingVersion: Content.Version, - prioritizing set: IndexSet?, isolation: isolated (any Actor) ) async { guard @@ -181,7 +177,7 @@ extension ThreePhaseRangeValidator { return } - let action = validator.beginValidation(of: target, prioritizing: set) + let action = validator.beginValidation(of: target) switch action { case .none: diff --git a/Sources/TreeSitterClient/BackgroundingLanguageLayerTree.swift b/Sources/TreeSitterClient/BackgroundingLanguageLayerTree.swift index bc7484e..6233f20 100644 --- a/Sources/TreeSitterClient/BackgroundingLanguageLayerTree.swift +++ b/Sources/TreeSitterClient/BackgroundingLanguageLayerTree.swift @@ -39,9 +39,11 @@ final class BackgroundingLanguageLayerTree { private var pendingOldPoint: Point? private let configuration: Configuration private let backgroundProcessor: BackgroundProcessor + public var rootLanguageConfiguration: LanguageConfiguration public init(rootLanguageConfig: LanguageConfiguration, configuration: Configuration) throws { self.configuration = configuration + self.rootLanguageConfiguration = rootLanguageConfig let rootLayer = try LanguageLayer(languageConfig: rootLanguageConfig, configuration: configuration.layerConfiguration) self.backgroundProcessor = BackgroundProcessor(value: rootLayer) diff --git a/Sources/TreeSitterClient/HybridSyncAsyncVersionedResource.swift b/Sources/TreeSitterClient/HybridSyncAsyncVersionedResource.swift index 2850f60..fdb3ca1 100644 --- a/Sources/TreeSitterClient/HybridSyncAsyncVersionedResource.swift +++ b/Sources/TreeSitterClient/HybridSyncAsyncVersionedResource.swift @@ -8,7 +8,7 @@ public struct HybridSyncAsyncLanguageLayer { extension HybridSyncAsyncValueProvider { func access( - isolation: isolated (any Actor)? = #isolation, + isolation: isolated (any Actor), input: sending Input, operation: @escaping (Bool, Output) throws -> sending Success, completion: @escaping (Result) -> Void @@ -29,7 +29,7 @@ extension HybridSyncAsyncValueProvider { _ = isolation do { - let output = try await self.async(input) + let output = try await self.async(isolation: isolation, input) let result = Result { try operation(false, output) } diff --git a/Sources/TreeSitterClient/TreeSitterClient.swift b/Sources/TreeSitterClient/TreeSitterClient.swift index 8644fe8..8a744e3 100644 --- a/Sources/TreeSitterClient/TreeSitterClient.swift +++ b/Sources/TreeSitterClient/TreeSitterClient.swift @@ -18,7 +18,8 @@ enum TreeSitterClientError: Error { /// /// Tree-sitter ultimately resolves to a single semantic view of the text, and is considered a single phase. However, it may require numerous validation/invalidation passes before fully resolving a document's content. /// -/// Today, compiler limitations mean that this type must be +/// Today, compiler limitations mean that this type must be MainActor. But I'm hoping that one can I can figure out how to lift that limitation. +@preconcurrency @MainActor @available(macOS 13.0, macCatalyst 16.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) public final class TreeSitterClient { @@ -96,16 +97,22 @@ public final class TreeSitterClient { changeHandler: { [unowned self] in self.didChange($0, completion: $1) } ) ) - private lazy var sublayerValidator = SublayerValidator( - configuration: .init( - versionedContent: versionedContent, - provider: validatorProvider - ), - isolation: MainActor.shared - ) + private lazy var sublayerValidator: SublayerValidator = { + let validator = SublayerValidator( + configuration: .init( + versionedContent: versionedContent, + provider: validatorProvider + ) + ) + + let rootName = self.rootLanguageConfiguration.name + + validator.name = "\(rootName)-sublayers" + + return validator + }() private let layerTree: BackgroundingLanguageLayerTree - private let queue = DispatchQueue(label: "com.chimehq.HybridTreeSitterClient") public init(rootLanguageConfig: LanguageConfiguration, configuration: Configuration) throws { self.configuration = configuration @@ -120,6 +127,10 @@ public final class TreeSitterClient { ) } + public var rootLanguageConfiguration: LanguageConfiguration { + layerTree.rootLanguageConfiguration + } + /// Prepare for a content change. /// /// This method must be called before any content changes have been applied that would affect how the `locationTransformer` configuration will behave. @@ -245,9 +256,11 @@ extension TreeSitterClient { private var validatorProvider: SublayerValidator.Provider { .init( + isolation: MainActor.shared, rangeProcessor: rangeProcessor, inputTransformer: { ($0.value.max, .optional) }, syncValue: { versioned in + print("validatorProvider sync") guard versioned.version == self.versionedContent.currentVersion else { return .stale } @@ -260,6 +273,7 @@ extension TreeSitterClient { }, asyncValue: { versioned in + print("validatorProvider async") guard versioned.version == self.versionedContent.currentVersion else { return .stale } @@ -279,6 +293,7 @@ extension TreeSitterClient { @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) extension TreeSitterClient { + @preconcurrency @MainActor public struct ClientQueryParams { public let indexSet: IndexSet @@ -302,6 +317,7 @@ extension TreeSitterClient { } } + @preconcurrency @MainActor public struct ClientQuery { public let query: Query.Definition @@ -329,7 +345,7 @@ extension TreeSitterClient { private func executeQuery(_ clientQuery: ClientQuery) async throws -> some Sequence { rangeProcessor.processLocation(clientQuery.params.maxLocation, mode: clientQuery.params.mode) - await rangeProcessor.processingCompleted() + await rangeProcessor.processingCompleted(isolation: MainActor.shared) validateSublayers(in: clientQuery.params.indexSet) @@ -340,12 +356,13 @@ extension TreeSitterClient { public var highlightsProvider: HighlightsProvider { .init( + isolation: MainActor.shared, rangeProcessor: rangeProcessor, inputTransformer: { ($0.maxLocation, $0.mode) }, syncValue: { input in let set = input.indexSet - guard self.canAttemptSynchronousAccess(in: .set(set)) else { return [] } + guard self.canAttemptSynchronousAccess(in: .set(set)) else { return nil } self.validateSublayers(in: set) @@ -363,7 +380,7 @@ extension TreeSitterClient { extension TreeSitterClient { /// Execute a standard highlights.scm query. public func highlights(in set: IndexSet, provider: @escaping TextProvider, mode: RangeFillMode = .required) async throws -> [NamedRange] { - try await highlightsProvider.async(.init(indexSet: set, textProvider: provider, mode: mode)) + try await highlightsProvider.async(isolation: MainActor.shared, .init(indexSet: set, textProvider: provider, mode: mode)) } /// Execute a standard highlights.scm query. @@ -373,6 +390,6 @@ extension TreeSitterClient { /// Execute a standard highlights.scm query. public func highlights(in range: NSRange, provider: @escaping TextProvider, mode: RangeFillMode = .required) async throws -> [NamedRange] { - try await highlightsProvider.async(.init(range: range, textProvider: provider, mode: mode)) + try await highlightsProvider.async(isolation: MainActor.shared, .init(range: range, textProvider: provider, mode: mode)) } } diff --git a/Tests/RangeStateTests/RangeProcessorTests.swift b/Tests/RangeStateTests/RangeProcessorTests.swift index d7350e9..3664468 100644 --- a/Tests/RangeStateTests/RangeProcessorTests.swift +++ b/Tests/RangeStateTests/RangeProcessorTests.swift @@ -225,4 +225,28 @@ final class RangeProcessorTests: XCTestCase { XCTAssertEqual(handler.mutations, expected) } + + @MainActor + func testWaitForDelayedProcessing() async throws { + let content = StringContent(string: "abcdefghij") + + let processor = RangeProcessor( + configuration: .init( + lengthProvider: { content.currentLength }, + changeHandler: { _, completion in + // I *think* that a single runloop turn will be enough + DispatchQueue.main.async() { + completion() + } + } + ) + ) + + // process everything, so there is no more filling needed when complete + XCTAssertFalse(processor.processLocation(10, mode: .required)) + + await processor.processingCompleted(isolation: MainActor.shared) + + XCTAssertTrue(processor.processed(10)) + } } diff --git a/Tests/RangeStateTests/SinglePhaseRangeValidatorTests.swift b/Tests/RangeStateTests/SinglePhaseRangeValidatorTests.swift index c407395..f87e133 100644 --- a/Tests/RangeStateTests/SinglePhaseRangeValidatorTests.swift +++ b/Tests/RangeStateTests/SinglePhaseRangeValidatorTests.swift @@ -125,4 +125,86 @@ final class SinglePhaseRangeValidatorTests: XCTestCase { content.string = "abcd" validator.contentChanged(in: NSRange(3..<3), delta: 1) } + + @MainActor + func testDistinctPrioritySetValidation() async { + let validationExp = expectation(description: "validation") + validationExp.expectedFulfillmentCount = 2 + var validatedRanges = [NSRange]() + + let content = StringContent(string: "aaabbbccc") + let provider = StringValidator.Provider( + syncValue: { _ in + return nil + }, + asyncValue: { _, contentRange in + validationExp.fulfill() + validatedRanges.append(contentRange.value) + + return .success(contentRange.value) + } + ) + + let validator = StringValidator( + configuration: .init( + versionedContent: content, + provider: provider + ) + ) + + let prioritySet = IndexSet(ranges: [ + NSRange(0..<3), + NSRange(6..<9), + ]) + + validator.validate(.set(prioritySet)) + await fulfillment(of: [validationExp], timeout: 1.0) + + XCTAssertEqual(validatedRanges, prioritySet.nsRangeView) + } + + @MainActor + func testDistinctInvalidRegions() async { + var validatedRanges = [NSRange]() + + let content = StringContent(string: "aaabbbccc") + let provider = StringValidator.Provider( + syncValue: { _ in + return nil + }, + asyncValue: { _, contentRange in + validatedRanges.append(contentRange.value) + + return .success(contentRange.value) + } + ) + + let validator = StringValidator( + configuration: .init( + versionedContent: content, + provider: provider + ) + ) + + validator.validate(.all) + await validator.validationCompleted(isolation: MainActor.shared) + + let set = IndexSet(ranges: [ + NSRange(0..<3), + NSRange(6..<9), + ]) + + // now we can invalidate two distinct regions + validator.invalidate(.set(set)) + validator.validate(.all) + + await validator.validationCompleted(isolation: MainActor.shared) + + let expectedRanges = [ + NSRange(0..<9), + NSRange(0..<3), + NSRange(6..<9), + ] + XCTAssertEqual(validatedRanges, expectedRanges) + } }