7
7
8
8
import Foundation
9
9
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
+ ///
11
37
final package class TreeSitterExecutor {
38
+ /// The priority of an operation. These are used to conditionally cancel operations. See ``TreeSitterExecutor/cancelAll(below:)``
12
39
enum Priority : Comparable {
13
40
case access
14
41
case edit
@@ -29,13 +56,17 @@ final package class TreeSitterExecutor {
29
56
case syncUnavailable
30
57
}
31
58
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.
32
63
@discardableResult
33
64
func execSync< T> ( _ operation: ( ) -> T ) -> Result < T , Error > {
34
65
guard let queueItemID = addSyncTask ( ) else {
35
66
return . failure( Error . syncUnavailable)
36
67
}
37
68
let returnVal = operation ( ) // Execute outside critical area.
38
- // Critical area , modifying the queue.
69
+ // Critical section , modifying the queue.
39
70
lock. withLock {
40
71
queuedTasks. removeAll ( where: { $0. id == queueItemID } )
41
72
}
@@ -53,12 +84,17 @@ final package class TreeSitterExecutor {
53
84
return id
54
85
}
55
86
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.
56
92
func execAsync( priority: Priority = . access, operation: @escaping ( ) -> Void , onCancel: @escaping ( ) -> Void ) {
57
- // Critical area , modifying the queue
93
+ // Critical section , modifying the queue
58
94
lock. lock ( )
59
95
defer { lock. unlock ( ) }
60
96
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__
62
98
while self . lock. withLock ( { !canTaskExec( id: id, priority: priority) } ) {
63
99
// Instead of yielding, sleeping frees up the CPU due to time off the CPU and less lock contention
64
100
try ? await Task . sleep ( for: TreeSitterClient . Constants. taskSleepDuration)
@@ -82,6 +118,7 @@ final package class TreeSitterExecutor {
82
118
}
83
119
84
120
removeTask ( id)
121
+ // __Back to outer lock control__
85
122
}
86
123
queuedTasks. append ( QueueItem ( task: task, id: id, priority: priority) )
87
124
}
@@ -92,7 +129,7 @@ final package class TreeSitterExecutor {
92
129
}
93
130
}
94
131
95
- /// Allow concurrent ``TreeSitterExecutor/Priority/access`` operations to run.
132
+ /// Allow concurrent ``TreeSitterExecutor/Priority/access`` operations to run. Thread safe.
96
133
private func canTaskExec( id: UUID , priority: Priority ) -> Bool {
97
134
if priority != . access {
98
135
return queuedTasks. first? . id == id
@@ -109,6 +146,11 @@ final package class TreeSitterExecutor {
109
146
return false
110
147
}
111
148
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.
112
154
func cancelAll( below priority: Priority ) {
113
155
lock. withLock {
114
156
queuedTasks. forEach { item in
0 commit comments