on 376c19e help sign in
helix/helix-vcs/src/diff/worker.rs
207 lines 7.4 kB history raw
1use std::mem::swap;
2use std::ops::Range;
3use std::sync::Arc;
5use helix_core::{Rope, RopeSlice};
6use imara_diff::intern::InternedInput;
7use parking_lot::Mutex;
8use tokio::sync::mpsc::UnboundedReceiver;
9use tokio::sync::Notify;
10use tokio::time::{timeout, timeout_at, Duration};
12use crate::diff::{
13 Event, RenderLock, ALGORITHM, DIFF_DEBOUNCE_TIME_ASYNC, DIFF_DEBOUNCE_TIME_SYNC,
14};
16use super::line_cache::InternedRopeLines;
17use super::Hunk;
19#[cfg(test)]
20mod test;
22pub(super) struct DiffWorker {
23 pub channel: UnboundedReceiver<Event>,
24 pub hunks: Arc<Mutex<Vec<Hunk>>>,
25 pub new_hunks: Vec<Hunk>,
26 pub redraw_notify: Arc<Notify>,
27 pub diff_finished_notify: Arc<Notify>,
30impl DiffWorker {
31 async fn accumulate_events(&mut self, event: Event) -> (Option<Rope>, Option<Rope>) {
32 let mut accumulator = EventAccumulator::new();
33 accumulator.handle_event(event).await;
34 accumulator
35 .accumulate_debounced_events(
36 &mut self.channel,
37 self.redraw_notify.clone(),
38 self.diff_finished_notify.clone(),
39 )
40 .await;
41 (accumulator.doc, accumulator.diff_base)
42 }
44 pub async fn run(mut self, diff_base: Rope, doc: Rope) {
45 let mut interner = InternedRopeLines::new(diff_base, doc);
46 if let Some(lines) = interner.interned_lines() {
47 self.perform_diff(lines);
48 }
49 self.apply_hunks();
50 while let Some(event) = self.channel.recv().await {
51 let (doc, diff_base) = self.accumulate_events(event).await;
53 let process_accumulated_events = || {
54 if let Some(new_base) = diff_base {
55 interner.update_diff_base(new_base, doc)
56 } else {
57 interner.update_doc(doc.unwrap())
58 }
60 if let Some(lines) = interner.interned_lines() {
61 self.perform_diff(lines)
62 }
63 };
65 // Calculating diffs is computationally expensive and should
66 // not run inside an async function to avoid blocking other futures.
67 // Note: tokio::task::block_in_place does not work during tests
68 #[cfg(test)]
69 process_accumulated_events();
70 #[cfg(not(test))]
71 tokio::task::block_in_place(process_accumulated_events);
73 self.apply_hunks();
74 }
75 }
77 /// update the hunks (used by the gutter) by replacing it with `self.new_hunks`.
78 /// `self.new_hunks` is always empty after this function runs.
79 /// To improve performance this function tries to reuse the allocation of the old diff previously stored in `self.line_diffs`
80 fn apply_hunks(&mut self) {
81 swap(&mut *self.hunks.lock(), &mut self.new_hunks);
82 self.diff_finished_notify.notify_waiters();
83 self.new_hunks.clear();
84 }
86 fn perform_diff(&mut self, input: &InternedInput<RopeSlice>) {
87 imara_diff::diff(ALGORITHM, input, |before: Range<u32>, after: Range<u32>| {
88 self.new_hunks.push(Hunk { before, after })
89 })
90 }
93struct EventAccumulator {
94 diff_base: Option<Rope>,
95 doc: Option<Rope>,
96 render_lock: Option<RenderLock>,
99impl<'a> EventAccumulator {
100 fn new() -> EventAccumulator {
101 EventAccumulator {
102 diff_base: None,
103 doc: None,
104 render_lock: None,
105 }
106 }
108 async fn handle_event(&mut self, event: Event) {
109 let dst = if event.is_base {
110 &mut self.diff_base
111 } else {
112 &mut self.doc
113 };
115 *dst = Some(event.text);
117 // always prefer the most synchronous requested render mode
118 if let Some(render_lock) = event.render_lock {
119 match &mut self.render_lock {
120 Some(RenderLock { timeout, .. }) => {
121 // A timeout of `None` means that the render should
122 // always wait for the diff to complete (so no timeout)
123 // remove the existing timeout, otherwise keep the previous timeout
124 // because it will be shorter then the current timeout
125 if render_lock.timeout.is_none() {
126 timeout.take();
127 }
128 }
129 None => self.render_lock = Some(render_lock),
130 }
131 }
132 }
134 async fn accumulate_debounced_events(
135 &mut self,
136 channel: &mut UnboundedReceiver<Event>,
137 redraw_notify: Arc<Notify>,
138 diff_finished_notify: Arc<Notify>,
139 ) {
140 let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
141 let sync_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_SYNC);
142 loop {
143 // if we are not blocking rendering use a much longer timeout
144 let debounce = if self.render_lock.is_none() {
145 async_debounce
146 } else {
147 sync_debounce
148 };
150 if let Ok(Some(event)) = timeout(debounce, channel.recv()).await {
151 self.handle_event(event).await;
152 } else {
153 break;
154 }
155 }
157 // setup task to trigger the rendering
158 match self.render_lock.take() {
159 // diff is performed outside of the rendering loop
160 // request a redraw after the diff is done
161 None => {
162 tokio::spawn(async move {
163 diff_finished_notify.notified().await;
164 redraw_notify.notify_one();
165 });
166 }
167 // diff is performed inside the rendering loop
168 // block redraw until the diff is done or the timeout is expired
169 Some(RenderLock {
170 lock,
171 timeout: Some(timeout),
172 }) => {
173 tokio::spawn(async move {
174 let res = {
175 // Acquire a lock on the redraw handle.
176 // The lock will block the rendering from occurring while held.
177 // The rendering waits for the diff if it doesn't time out
178 timeout_at(timeout, diff_finished_notify.notified()).await
179 };
180 // we either reached the timeout or the diff is finished, release the render lock
181 drop(lock);
182 if res.is_ok() {
183 // Diff finished in time we are done.
184 return;
185 }
186 // Diff failed to complete in time log the event
187 // and wait until the diff occurs to trigger an async redraw
188 log::info!("Diff computation timed out, update of diffs might appear delayed");
189 diff_finished_notify.notified().await;
190 redraw_notify.notify_one();
191 });
192 }
193 // a blocking diff is performed inside the rendering loop
194 // block redraw until the diff is done
195 Some(RenderLock {
196 lock,
197 timeout: None,
198 }) => {
199 tokio::spawn(async move {
200 diff_finished_notify.notified().await;
201 // diff is done release the lock
202 drop(lock)
203 });
204 }
205 };
206 }