diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 4b37c2dbfafdc2dc1886c81993885c5cf53a0d2e..45fd1fb743fdee8d00861d8d19e0cdc3370e7266 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -4,7 +4,7 @@ use crate::{ keymap::{self, Keystroke}, platform::{self, WindowOptions}, presenter::Presenter, - util::post_inc, + util::{post_inc, timeout}, AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; @@ -25,6 +25,7 @@ use std::{ path::PathBuf, rc::{self, Rc}, sync::{Arc, Weak}, + time::Duration, }; pub trait Entity: 'static + Send + Sync { @@ -2007,18 +2008,23 @@ impl ModelHandle { let handle = self.clone(); async move { - loop { - { - let ctx = ctx.borrow(); - let ctx = ctx.as_ref(); - if predicate(handle.read(ctx), ctx) { - break; + timeout(Duration::from_millis(200), async move { + loop { + { + let ctx = ctx.borrow(); + let ctx = ctx.as_ref(); + if predicate(handle.read(ctx), ctx) { + break; + } + } + + if rx.recv().await.is_none() { + panic!("model dropped with pending condition"); } } - if rx.recv().await.is_none() { - break; - } - } + }) + .await + .expect("condition timed out"); } } } @@ -2170,18 +2176,23 @@ impl ViewHandle { let handle = self.clone(); async move { - loop { - { - let ctx = ctx.borrow(); - let ctx = ctx.as_ref(); - if predicate(handle.read(ctx), ctx) { - break; + timeout(Duration::from_millis(200), async move { + loop { + { + let ctx = ctx.borrow(); + let ctx = ctx.as_ref(); + if predicate(handle.read(ctx), ctx) { + break; + } + } + + if rx.recv().await.is_none() { + panic!("model dropped with pending condition"); } } - if rx.recv().await.is_none() { - break; - } - } + }) + .await + .expect("condition timed out"); } } } @@ -2475,6 +2486,7 @@ impl Drop for EntityTask { mod tests { use super::*; use crate::elements::*; + use smol::future::poll_once; #[test] fn test_model_handles() { @@ -3276,6 +3288,126 @@ mod tests { }); } + #[test] + fn test_model_condition() { + struct Counter(usize); + + impl super::Entity for Counter { + type Event = (); + } + + impl Counter { + fn inc(&mut self, ctx: &mut ModelContext) { + self.0 += 1; + ctx.notify(); + } + } + + App::test_async((), |mut app| async move { + let model = app.add_model(|_| Counter(0)); + + let condition1 = model.condition(&app, |model, _| model.0 == 2); + let condition2 = model.condition(&app, |model, _| model.0 == 3); + smol::pin!(condition1, condition2); + + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, None); + assert_eq!(poll_once(&mut condition2).await, None); + + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, Some(())); + assert_eq!(poll_once(&mut condition2).await, None); + + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition2).await, Some(())); + }); + } + + #[test] + #[should_panic] + fn test_model_condition_timeout() { + struct Model; + + impl super::Entity for Model { + type Event = (); + } + + App::test_async((), |mut app| async move { + let model = app.add_model(|_| Model); + model.condition(&app, |_, _| false).await; + }); + } + + #[test] + fn test_view_condition() { + struct Counter(usize); + + impl super::Entity for Counter { + type Event = (); + } + + impl super::View for Counter { + fn ui_name() -> &'static str { + "test view" + } + + fn render(&self, _: &AppContext) -> ElementBox { + Empty::new().boxed() + } + } + + impl Counter { + fn inc(&mut self, ctx: &mut ViewContext) { + self.0 += 1; + ctx.notify(); + } + } + + App::test_async((), |mut app| async move { + let (_, view) = app.add_window(|_| Counter(0)); + + let condition1 = view.condition(&app, |view, _| view.0 == 2); + let condition2 = view.condition(&app, |view, _| view.0 == 3); + smol::pin!(condition1, condition2); + + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, None); + assert_eq!(poll_once(&mut condition2).await, None); + + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, Some(())); + assert_eq!(poll_once(&mut condition2).await, None); + + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition2).await, Some(())); + }); + } + + #[test] + #[should_panic] + fn test_view_condition_timeout() { + struct View; + + impl super::Entity for View { + type Event = (); + } + + impl super::View for View { + fn ui_name() -> &'static str { + "test view" + } + + fn render(&self, _: &AppContext) -> ElementBox { + Empty::new().boxed() + } + } + + App::test_async((), |mut app| async move { + let (_, view) = app.add_window(|_| View); + view.condition(&app, |_, _| false).await; + }); + } + // #[test] // fn test_ui_and_window_updates() { // struct View { diff --git a/gpui/src/util.rs b/gpui/src/util.rs index 473c8d00f13f40762feaddc0503ac77ee6571a6c..10731ced5cc136a5aa48224a9746efabadfbacfc 100644 --- a/gpui/src/util.rs +++ b/gpui/src/util.rs @@ -1,5 +1,20 @@ +use smol::future::FutureExt; +use std::{future::Future, time::Duration}; + pub fn post_inc(value: &mut usize) -> usize { let prev = *value; *value += 1; prev } + +pub async fn timeout(timeout: Duration, f: F) -> Result +where + F: Future, +{ + let timer = async { + smol::Timer::after(timeout).await; + Err(()) + }; + let future = async move { Ok(f.await) }; + timer.race(future).await +} diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index d098bb7d3af696f1bda041826c1180f4d6889367..4b0cd1df537b3c9cbe07f54f2b2238181eeb2d6b 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -77,9 +77,9 @@ impl Worktree { pub fn scan_complete(&self) -> impl Future { let mut scan_state_rx = self.scan_state.1.clone(); async move { - let mut next_scan_state = Some(scan_state_rx.borrow().clone()); - while let Some(ScanState::Scanning) = next_scan_state { - next_scan_state = scan_state_rx.recv().await; + let mut scan_state = Some(scan_state_rx.borrow().clone()); + while let Some(ScanState::Scanning) = scan_state { + scan_state = scan_state_rx.recv().await; } } }