From ea544e5a17c9249ce35715654dbd81d81a3e1bd1 Mon Sep 17 00:00:00 2001 From: novakduc Date: Sun, 20 Sep 2026 12:14:06 +0700 Subject: [PATCH 1/2] input: Clear the blink state when the cursor stops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BlinkCursor::stop`, called from `on_blur`, resets the epoch but leaves `paused` and `visible` holding whatever the last `pause` set. A blur inside the 300ms pause window — tabbing away right after a keystroke — therefore leaves `paused == true` on a stopped cursor. The next `start` then hits `blink`'s `if self.paused` early return and never moves the epoch off zero, so the refocused input has a cursor that is not blinking. The defect is masked today, and masked by accident: `pause` acts on any cursor, blinking or not, so the next keystroke starts the loop again. That accident is itself a bug — it is what makes a programmatic `set_value` blink an unfocused input forever — and the next commit removes it. This one has to land first so that removing the mask does not turn a hidden defect into a visible one. Clear `paused` and `visible` alongside the epoch, so a stopped cursor carries nothing into the next focus. As a side effect the caret now appears the moment an input is focused, rather than starting from whatever `visible` the previous focus left behind. novakduc@arch --- crates/base/src/input/base/blink_cursor.rs | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/base/src/input/base/blink_cursor.rs b/crates/base/src/input/base/blink_cursor.rs index cece966321..3a8f82b0ae 100644 --- a/crates/base/src/input/base/blink_cursor.rs +++ b/crates/base/src/input/base/blink_cursor.rs @@ -39,8 +39,12 @@ impl BlinkCursor { self.blink(self.epoch, cx); } + /// Stop the blinking and clear the blink state, so the next [`Self::start`] + /// begins from a visible cursor instead of resuming a stale pause. pub(crate) fn stop(&mut self, cx: &mut Context) { self.epoch = 0; + self.paused = false; + self.visible = false; cx.notify(); } @@ -117,4 +121,28 @@ mod tests { cx.run_until_parked(); assert!(cursor.read_with(cx, |cursor, _| cursor.visible())); } + + #[gpui::test] + fn blurring_a_paused_cursor_leaves_the_next_focus_blinking(cx: &mut TestAppContext) { + let cursor = cx.new(|_| BlinkCursor::new()); + cursor.update(cx, |cursor, cx| cursor.start(cx)); + cx.run_until_parked(); + + // Typing pauses the blink, then the input is blurred before the pause + // elapses: tabbing away right after a keystroke does exactly this. + cursor.update(cx, |cursor, cx| cursor.pause(cx)); + cx.run_until_parked(); + cursor.update(cx, |cursor, cx| cursor.stop(cx)); + cx.run_until_parked(); + assert!(!cursor.read_with(cx, |cursor, _| cursor.visible())); + + // Focusing again shows the cursor and blinks it, rather than leaving a + // stale pause to swallow the start. + cursor.update(cx, |cursor, cx| cursor.start(cx)); + cx.run_until_parked(); + assert!(cursor.read_with(cx, |cursor, _| cursor.visible())); + cx.executor().advance_clock(INTERVAL); + cx.run_until_parked(); + assert!(!cursor.read_with(cx, |cursor, _| cursor.visible())); + } } From b81d6bc14c0867db8a139176a77518b2eafaf398 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Sun, 20 Sep 2026 20:30:34 +0800 Subject: [PATCH 2/2] input: Cancel stale blink tasks when stopping --- crates/base/src/input/base/blink_cursor.rs | 59 +++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/crates/base/src/input/base/blink_cursor.rs b/crates/base/src/input/base/blink_cursor.rs index 3a8f82b0ae..74113adf97 100644 --- a/crates/base/src/input/base/blink_cursor.rs +++ b/crates/base/src/input/base/blink_cursor.rs @@ -45,6 +45,7 @@ impl BlinkCursor { self.epoch = 0; self.paused = false; self.visible = false; + self._task = Task::ready(()); cx.notify(); } @@ -54,7 +55,12 @@ impl BlinkCursor { } fn blink(&mut self, epoch: usize, cx: &mut Context) { - if self.paused || epoch != self.epoch { + // A task from an earlier blink lifecycle must not mutate the current state. + if epoch != self.epoch { + return; + } + + if self.paused { self.visible = true; return; } @@ -90,6 +96,10 @@ impl BlinkCursor { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { + if epoch != this.epoch { + return; + } + this.paused = false; this.blink(epoch, cx); }); @@ -102,6 +112,7 @@ impl BlinkCursor { mod tests { use super::*; use gpui::{AppContext as _, TestAppContext}; + use std::{cell::Cell, rc::Rc}; #[gpui::test] fn repeated_pauses_keep_cursor_visible_until_idle(cx: &mut TestAppContext) { @@ -145,4 +156,50 @@ mod tests { cx.run_until_parked(); assert!(!cursor.read_with(cx, |cursor, _| cursor.visible())); } + + #[gpui::test] + fn stopping_a_paused_cursor_ends_the_blink_loop(cx: &mut TestAppContext) { + let cursor = cx.new(|_| BlinkCursor::new()); + cursor.update(cx, |cursor, cx| cursor.start(cx)); + cx.run_until_parked(); + cursor.update(cx, |cursor, cx| cursor.pause(cx)); + cx.run_until_parked(); + cursor.update(cx, |cursor, cx| cursor.stop(cx)); + cx.run_until_parked(); + + let notifications = Rc::new(Cell::new(0usize)); + let count = notifications.clone(); + let _observer = cx.update(|cx| cx.observe(&cursor, move |_, _| count.set(count.get() + 1))); + cx.run_until_parked(); + + cx.executor().advance_clock(Duration::from_secs(3)); + cx.run_until_parked(); + + assert!(!cursor.read_with(cx, |cursor, _| cursor.visible())); + assert_eq!(notifications.get(), 0, "a stopped cursor kept blinking"); + + cursor.update(cx, |cursor, cx| cursor.start(cx)); + cx.run_until_parked(); + assert!(cursor.read_with(cx, |cursor, _| cursor.visible())); + } + + #[gpui::test] + fn stopping_a_blinking_cursor_ends_the_blink_loop(cx: &mut TestAppContext) { + let cursor = cx.new(|_| BlinkCursor::new()); + cursor.update(cx, |cursor, cx| cursor.start(cx)); + cx.run_until_parked(); + cursor.update(cx, |cursor, cx| cursor.stop(cx)); + cx.run_until_parked(); + + let notifications = Rc::new(Cell::new(0usize)); + let count = notifications.clone(); + let _observer = cx.update(|cx| cx.observe(&cursor, move |_, _| count.set(count.get() + 1))); + cx.run_until_parked(); + + cx.executor().advance_clock(Duration::from_secs(3)); + cx.run_until_parked(); + + assert!(!cursor.read_with(cx, |cursor, _| cursor.visible())); + assert_eq!(notifications.get(), 0, "a stopped cursor kept blinking"); + } }