Merge branch 'jk/no-common'
[git] / git-gui / lib / chord.tcl
1 # Simple Chord for Tcl
2 #
3 # A "chord" is a method with more than one entrypoint and only one body, such
4 # that the body runs only once all the entrypoints have been called by
5 # different asynchronous tasks. In this implementation, the chord is defined
6 # dynamically for each invocation. A SimpleChord object is created, supplying
7 # body script to be run when the chord is completed, and then one or more notes
8 # are added to the chord. Each note can be called like a proc, and returns
9 # immediately if the chord isn't yet complete. When the last remaining note is
10 # called, the body runs before the note returns.
11 #
12 # The SimpleChord class has a constructor that takes the body script, and a
13 # method add_note that returns a note object. Since the body script does not
14 # run in the context of the procedure that defined it, a mechanism is provided
15 # for injecting variables into the chord for use by the body script. The
16 # activation of a note is idempotent; multiple calls have the same effect as
17 # a simple call.
18 #
19 # If you are invoking asynchronous operations with chord notes as completion
20 # callbacks, and there is a possibility that earlier operations could complete
21 # before later ones are started, it is a good practice to create a "common"
22 # note on the chord that prevents it from being complete until you're certain
23 # you've added all the notes you need.
24 #
25 # Example:
26 #
27 #   # Turn off the UI while running a couple of async operations.
28 #   lock_ui
29 #
30 #   set chord [SimpleChord::new {
31 #     unlock_ui
32 #     # Note: $notice here is not referenced in the calling scope
33 #     if {$notice} { info_popup $notice }
34 #   }
35 #
36 #   # Configure a note to keep the chord from completing until
37 #   # all operations have been initiated.
38 #   set common_note [$chord add_note]
39 #
40 #   # Activate notes in 'after' callbacks to other operations
41 #   set newnote [$chord add_note]
42 #   async_operation $args [list $newnote activate]
43 #
44 #   # Communicate with the chord body
45 #   if {$condition} {
46 #     # This sets $notice in the same context that the chord body runs in.
47 #     $chord eval { set notice "Something interesting" }
48 #   }
49 #
50 #   # Activate the common note, making the chord eligible to complete
51 #   $common_note activate
52 #
53 # At this point, the chord will complete at some unknown point in the future.
54 # The common note might have been the first note activated, or the async
55 # operations might have completed synchronously and the common note is the
56 # last one, completing the chord before this code finishes, or anything in
57 # between. The purpose of the chord is to not have to worry about the order.
58
59 # SimpleChord class:
60 #   Represents a procedure that conceptually has multiple entrypoints that must
61 #   all be called before the procedure executes. Each entrypoint is called a
62 #   "note". The chord is only "completed" when all the notes are "activated".
63 class SimpleChord {
64         field notes
65         field body
66         field is_completed
67         field eval_ns
68
69         # Constructor:
70         #   set chord [SimpleChord::new {body}]
71         #     Creates a new chord object with the specified body script. The
72         #     body script is evaluated at most once, when a note is activated
73         #     and the chord has no other non-activated notes.
74         constructor new {i_body} {
75                 set notes [list]
76                 set body $i_body
77                 set is_completed 0
78                 set eval_ns "[namespace qualifiers $this]::eval"
79                 return $this
80         }
81
82         # Method:
83         #   $chord eval {script}
84         #     Runs the specified script in the same context (namespace) in which
85         #     the chord body will be evaluated. This can be used to set variable
86         #     values for the chord body to use.
87         method eval {script} {
88                 namespace eval $eval_ns $script
89         }
90
91         # Method:
92         #   set note [$chord add_note]
93         #     Adds a new note to the chord, an instance of ChordNote. Raises an
94         #     error if the chord is already completed, otherwise the chord is
95         #     updated so that the new note must also be activated before the
96         #     body is evaluated.
97         method add_note {} {
98                 if {$is_completed} { error "Cannot add a note to a completed chord" }
99
100                 set note [ChordNote::new $this]
101
102                 lappend notes $note
103
104                 return $note
105         }
106
107         # This method is for internal use only and is intentionally undocumented.
108         method notify_note_activation {} {
109                 if {!$is_completed} {
110                         foreach note $notes {
111                                 if {![$note is_activated]} { return }
112                         }
113
114                         set is_completed 1
115
116                         namespace eval $eval_ns $body
117                         delete_this
118                 }
119         }
120 }
121
122 # ChordNote class:
123 #   Represents a note within a chord, providing a way to activate it. When the
124 #   final note of the chord is activated (this can be any note in the chord,
125 #   with all other notes already previously activated in any order), the chord's
126 #   body is evaluated.
127 class ChordNote {
128         field chord
129         field is_activated
130
131         # Constructor:
132         #   Instances of ChordNote are created internally by calling add_note on
133         #   SimpleChord objects.
134         constructor new {c} {
135                 set chord $c
136                 set is_activated 0
137                 return $this
138         }
139
140         # Method:
141         #   [$note is_activated]
142         #     Returns true if this note has already been activated.
143         method is_activated {} {
144                 return $is_activated
145         }
146
147         # Method:
148         #   $note activate
149         #     Activates the note, if it has not already been activated, and
150         #     completes the chord if there are no other notes awaiting
151         #     activation. Subsequent calls will have no further effect.
152         method activate {} {
153                 if {!$is_activated} {
154                         set is_activated 1
155                         $chord notify_note_activation
156                 }
157         }
158 }