77
88import Foundation
99
10- /// A class for managing
10+ /// This class provides a thread-safe API for executing `tree-sitter` operations synchronously or asynchronously.
11+ ///
12+ /// `tree-sitter` can take a potentially long time to parse a document. Long enough that we may decide to free up the
13+ /// main thread and do syntax highlighting when the parse is complete. To accomplish this, the ``TreeSitterClient``
14+ /// uses a ``TreeSitterExecutor`` to perform both sync and async operations.
15+ ///
16+ /// Sync operations occur when the ``TreeSitterClient`` _both_ a) estimates that a query or parse will not take too
17+ /// long on the main thread to gum up interactions, and b) there are no async operations already in progress. If either
18+ /// condition is false, the operation must be performed asynchronously or is cancelled. Finally, async operations may
19+ /// need to be cancelled, and should cancel quickly based on the level of access required for the operation
20+ /// (see ``TreeSitterExecutor/Priority``).
21+ ///
22+ /// The ``TreeSitterExecutor`` facilitates this requirement by providing a simple API that ``TreeSitterClient`` can use
23+ /// to attempt sync operations, queue async operations, and cancel async operations. It does this by managing a queue
24+ /// of tasks to execute in order. Each task is given a priority when queued and all queue operations are made thread
25+ /// safe using a lock.
26+ ///
27+ /// To check if a sync operation can occur, the queue is checked. If empty or the lock could not be acquired, the sync
28+ /// operation is queued without a swift `Task` and executed. This forces parallel sync attempts to be made async and
29+ /// will run after the original sync operation is finished.
30+ ///
31+ /// Async operations are added to the queue in a detached `Task`. Before they execute their operation callback, they
32+ /// first ensure they are next in the queue. This is done by acquiring the queue lock and checking the queue contents.
33+ /// To avoid lock contention (and essentially implementing a spinlock), the task sleeps for a few milliseconds
34+ /// (defined by ``TreeSitterClient/Constants/taskSleepDuration``) after failing to be next in the queue. Once up for
35+ /// running, the operation is executed. Finally, the lock is acquired again and the task is removed from the queue.
36+ ///
1137final package class TreeSitterExecutor {
38+ /// The priority of an operation. These are used to conditionally cancel operations. See ``TreeSitterExecutor/cancelAll(below:)``
1239 enum Priority : Comparable {
1340 case access
1441 case edit
@@ -29,13 +56,17 @@ final package class TreeSitterExecutor {
2956 case syncUnavailable
3057 }
3158
59+ /// Attempt to execute a synchronous operation. Thread safe.
60+ /// - Parameter operation: The callback to execute.
61+ /// - Returns: Returns a `.failure` with a ``TreeSitterExecutor/Error/syncUnavailable`` error if the operation
62+ /// cannot be safely performed synchronously.
3263 @discardableResult
3364 func execSync< T> ( _ operation: ( ) -> T ) -> Result < T , Error > {
3465 guard let queueItemID = addSyncTask ( ) else {
3566 return . failure( Error . syncUnavailable)
3667 }
3768 let returnVal = operation ( ) // Execute outside critical area.
38- // Critical area , modifying the queue.
69+ // Critical section , modifying the queue.
3970 lock. withLock {
4071 queuedTasks. removeAll ( where: { $0. id == queueItemID } )
4172 }
@@ -53,12 +84,17 @@ final package class TreeSitterExecutor {
5384 return id
5485 }
5586
87+ /// Execute an operation asynchronously. Thread safe.
88+ /// - Parameters:
89+ /// - priority: The priority given to the operation. Defaults to ``TreeSitterExecutor/Priority/access``.
90+ /// - operation: The operation to execute. It is up to the caller to exit _ASAP_ if the task is cancelled.
91+ /// - onCancel: A callback called if the operation was cancelled.
5692 func execAsync( priority: Priority = . access, operation: @escaping ( ) -> Void , onCancel: @escaping ( ) -> Void ) {
57- // Critical area , modifying the queue
93+ // Critical section , modifying the queue
5894 lock. lock ( )
5995 defer { lock. unlock ( ) }
6096 let id = UUID ( )
61- let task = Task ( priority: . userInitiated) { // This executes outside the lock's control.
97+ let task = Task ( priority: . userInitiated) { // __This executes outside the outer lock's control__
6298 while self . lock. withLock ( { !canTaskExec( id: id, priority: priority) } ) {
6399 // Instead of yielding, sleeping frees up the CPU due to time off the CPU and less lock contention
64100 try ? await Task . sleep ( for: TreeSitterClient . Constants. taskSleepDuration)
@@ -82,6 +118,7 @@ final package class TreeSitterExecutor {
82118 }
83119
84120 removeTask ( id)
121+ // __Back to outer lock control__
85122 }
86123 queuedTasks. append ( QueueItem ( task: task, id: id, priority: priority) )
87124 }
@@ -92,7 +129,7 @@ final package class TreeSitterExecutor {
92129 }
93130 }
94131
95- /// Allow concurrent ``TreeSitterExecutor/Priority/access`` operations to run.
132+ /// Allow concurrent ``TreeSitterExecutor/Priority/access`` operations to run. Thread safe.
96133 private func canTaskExec( id: UUID , priority: Priority ) -> Bool {
97134 if priority != . access {
98135 return queuedTasks. first? . id == id
@@ -109,6 +146,11 @@ final package class TreeSitterExecutor {
109146 return false
110147 }
111148
149+ /// Cancels all queued or running tasks below the given priority. Thread safe.
150+ /// - Note: Does not guarantee work stops immediately. It is up to the caller to provide callbacks that exit
151+ /// ASAP when a task is cancelled.
152+ /// - Parameter priority: The priority to cancel below. Eg: if given `reset`, will cancel all `edit` and `access`
153+ /// operations.
112154 func cancelAll( below priority: Priority ) {
113155 lock. withLock {
114156 queuedTasks. forEach { item in
0 commit comments