Pull blink functionality out of editor and into blink manager. Make blink manager subscribe to settings changes in order to start blinking properly when it is re-enabled.

K Simmons and Mikayla Maki created

Co-Authored-By: Mikayla Maki <mikayla@zed.dev>

Change summary

assets/settings/default.json       |   4 
crates/editor/src/blink_manager.rs | 110 ++++++++++++++++++++++++++++++++
crates/editor/src/editor.rs        |  84 +++--------------------
crates/editor/src/element.rs       |   2 
crates/settings/src/settings.rs    |  13 +--
5 files changed, 131 insertions(+), 82 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -10,6 +10,8 @@
     // Whether to show the informational hover box when moving the mouse
     // over symbols in the editor.
     "hover_popover_enabled": true,
+    // Whether the cursor blinks in the editor.
+    "cursor_blink": true,
     // Whether to pop the completions menu while typing in an editor without
     // explicitly requesting it.
     "show_completions_on_input": true,
@@ -69,8 +71,6 @@
     // The column at which to soft-wrap lines, for buffers where soft-wrap
     // is enabled.
     "preferred_line_length": 80,
-    // Whether the cursor blinks in the editor.
-    "cursor_blink": true,
     // Whether to indent lines using tab characters, as opposed to multiple
     // spaces.
     "hard_tabs": false,
@@ -0,0 +1,110 @@
+use std::time::Duration;
+
+use gpui::{Entity, ModelContext};
+use settings::Settings;
+use smol::Timer;
+
+pub struct BlinkManager {
+    blink_interval: Duration,
+
+    blink_epoch: usize,
+    blinking_paused: bool,
+    visible: bool,
+    enabled: bool,
+}
+
+impl BlinkManager {
+    pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
+        let weak_handle = cx.weak_handle();
+        cx.observe_global::<Settings, _>(move |_, cx| {
+            if let Some(this) = weak_handle.upgrade(cx) {
+                // Make sure we blink the cursors if the setting is re-enabled
+                this.update(cx, |this, cx| this.blink_cursors(this.blink_epoch, cx));
+            }
+        })
+        .detach();
+
+        Self {
+            blink_interval,
+
+            blink_epoch: 0,
+            blinking_paused: false,
+            visible: true,
+            enabled: true,
+        }
+    }
+
+    fn next_blink_epoch(&mut self) -> usize {
+        self.blink_epoch += 1;
+        self.blink_epoch
+    }
+
+    pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
+        if !self.visible {
+            self.visible = true;
+            cx.notify();
+        }
+
+        let epoch = self.next_blink_epoch();
+        let interval = self.blink_interval;
+        cx.spawn(|this, mut cx| {
+            let this = this.downgrade();
+            async move {
+                Timer::after(interval).await;
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
+                }
+            }
+        })
+        .detach();
+    }
+
+    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
+        if epoch == self.blink_epoch {
+            self.blinking_paused = false;
+            self.blink_cursors(epoch, cx);
+        }
+    }
+
+    fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
+        if cx.global::<Settings>().cursor_blink {
+            if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
+                self.visible = !self.visible;
+                cx.notify();
+
+                let epoch = self.next_blink_epoch();
+                let interval = self.blink_interval;
+                cx.spawn(|this, mut cx| {
+                    let this = this.downgrade();
+                    async move {
+                        Timer::after(interval).await;
+                        if let Some(this) = this.upgrade(&cx) {
+                            this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
+                        }
+                    }
+                })
+                .detach();
+            }
+        } else if !self.visible {
+            self.visible = true;
+            cx.notify();
+        }
+    }
+
+    pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
+        self.enabled = true;
+        self.blink_cursors(self.blink_epoch, cx);
+    }
+
+    pub fn disable(&mut self, _: &mut ModelContext<Self>) {
+        self.enabled = true;
+    }
+
+    pub fn visible(&self) -> bool {
+        self.visible
+    }
+}
+
+impl Entity for BlinkManager {
+    type Event = ();
+}

crates/editor/src/editor.rs 🔗

@@ -1,3 +1,4 @@
+mod blink_manager;
 pub mod display_map;
 mod element;
 mod highlight_matching_bracket;
@@ -16,6 +17,7 @@ pub mod test;
 
 use aho_corasick::AhoCorasick;
 use anyhow::Result;
+use blink_manager::BlinkManager;
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 pub use display_map::DisplayPoint;
@@ -447,12 +449,10 @@ pub struct Editor {
     override_text_style: Option<Box<OverrideTextStyle>>,
     project: Option<ModelHandle<Project>>,
     focused: bool,
-    show_local_cursors: bool,
+    blink_manager: ModelHandle<BlinkManager>,
     show_local_selections: bool,
     show_scrollbars: bool,
     hide_scrollbar_task: Option<Task<()>>,
-    blink_epoch: usize,
-    blinking_paused: bool,
     mode: EditorMode,
     vertical_scroll_margin: f32,
     placeholder_text: Option<Arc<str>>,
@@ -1076,6 +1076,8 @@ impl Editor {
 
         let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
 
+        let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
+
         let mut this = Self {
             handle: cx.weak_handle(),
             buffer: buffer.clone(),
@@ -1097,12 +1099,10 @@ impl Editor {
             scroll_top_anchor: Anchor::min(),
             autoscroll_request: None,
             focused: false,
-            show_local_cursors: false,
+            blink_manager: blink_manager.clone(),
             show_local_selections: true,
             show_scrollbars: true,
             hide_scrollbar_task: None,
-            blink_epoch: 0,
-            blinking_paused: false,
             mode,
             vertical_scroll_margin: 3.0,
             placeholder_text: None,
@@ -1130,6 +1130,7 @@ impl Editor {
                 cx.observe(&buffer, Self::on_buffer_changed),
                 cx.subscribe(&buffer, Self::on_buffer_event),
                 cx.observe(&display_map, Self::on_display_map_changed),
+                cx.observe(&blink_manager, |_, _, cx| cx.notify()),
             ],
         };
         this.end_selection(cx);
@@ -1542,7 +1543,7 @@ impl Editor {
             refresh_matching_bracket_highlights(self, cx);
         }
 
-        self.pause_cursor_blinking(cx);
+        self.blink_manager.update(cx, BlinkManager::pause_blinking);
         cx.emit(Event::SelectionsChanged { local });
         cx.notify();
     }
@@ -6111,70 +6112,8 @@ impl Editor {
         highlights
     }
 
-    fn next_blink_epoch(&mut self) -> usize {
-        self.blink_epoch += 1;
-        self.blink_epoch
-    }
-
-    fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
-        if !self.focused {
-            return;
-        }
-
-        self.show_local_cursors = true;
-        cx.notify();
-
-        let epoch = self.next_blink_epoch();
-        cx.spawn(|this, mut cx| {
-            let this = this.downgrade();
-            async move {
-                Timer::after(CURSOR_BLINK_INTERVAL).await;
-                if let Some(this) = this.upgrade(&cx) {
-                    this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
-                }
-            }
-        })
-        .detach();
-    }
-
-    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
-        if epoch == self.blink_epoch {
-            self.blinking_paused = false;
-            self.blink_cursors(epoch, cx);
-        }
-    }
-
-    fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
-        if epoch == self.blink_epoch && self.focused && !self.blinking_paused {
-            let newest_head = self.selections.newest::<usize>(cx).head();
-            let language_name = self
-                .buffer
-                .read(cx)
-                .language_at(newest_head, cx)
-                .map(|l| l.name());
-
-            self.show_local_cursors = !self.show_local_cursors
-                || !cx
-                    .global::<Settings>()
-                    .cursor_blink(language_name.as_deref());
-            cx.notify();
-
-            let epoch = self.next_blink_epoch();
-            cx.spawn(|this, mut cx| {
-                let this = this.downgrade();
-                async move {
-                    Timer::after(CURSOR_BLINK_INTERVAL).await;
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
-                    }
-                }
-            })
-            .detach();
-        }
-    }
-
-    pub fn show_local_cursors(&self) -> bool {
-        self.show_local_cursors && self.focused
+    pub fn show_local_cursors(&self, cx: &AppContext) -> bool {
+        self.blink_manager.read(cx).visible() && self.focused
     }
 
     pub fn show_scrollbars(&self) -> bool {
@@ -6493,7 +6432,7 @@ impl View for Editor {
             cx.focus(&rename.editor);
         } else {
             self.focused = true;
-            self.blink_cursors(self.blink_epoch, cx);
+            self.blink_manager.update(cx, BlinkManager::enable);
             self.buffer.update(cx, |buffer, cx| {
                 buffer.finalize_last_transaction(cx);
                 if self.leader_replica_id.is_none() {
@@ -6512,6 +6451,7 @@ impl View for Editor {
         let blurred_event = EditorBlurred(cx.handle());
         cx.emit_global(blurred_event);
         self.focused = false;
+        self.blink_manager.update(cx, BlinkManager::disable);
         self.buffer
             .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
         self.hide_context_menu(cx);

crates/editor/src/element.rs 🔗

@@ -705,7 +705,7 @@ impl EditorElement {
                     cx,
                 );
 
-                if view.show_local_cursors() || *replica_id != local_replica_id {
+                if view.show_local_cursors(cx) || *replica_id != local_replica_id {
                     let cursor_position = selection.head;
                     if layout
                         .visible_display_row_range

crates/settings/src/settings.rs 🔗

@@ -28,6 +28,7 @@ pub struct Settings {
     pub buffer_font_family: FamilyId,
     pub default_buffer_font_size: f32,
     pub buffer_font_size: f32,
+    pub cursor_blink: bool,
     pub hover_popover_enabled: bool,
     pub show_completions_on_input: bool,
     pub vim_mode: bool,
@@ -79,7 +80,6 @@ pub struct GitGutterConfig {}
 pub struct EditorSettings {
     pub tab_size: Option<NonZeroU32>,
     pub hard_tabs: Option<bool>,
-    pub cursor_blink: Option<bool>,
     pub soft_wrap: Option<SoftWrap>,
     pub preferred_line_length: Option<u32>,
     pub format_on_save: Option<FormatOnSave>,
@@ -235,6 +235,8 @@ pub struct SettingsFileContent {
     #[serde(default)]
     pub buffer_font_size: Option<f32>,
     #[serde(default)]
+    pub cursor_blink: Option<bool>,
+    #[serde(default)]
     pub hover_popover_enabled: Option<bool>,
     #[serde(default)]
     pub show_completions_on_input: Option<bool>,
@@ -293,6 +295,7 @@ impl Settings {
                 .unwrap(),
             buffer_font_size: defaults.buffer_font_size.unwrap(),
             default_buffer_font_size: defaults.buffer_font_size.unwrap(),
+            cursor_blink: defaults.cursor_blink.unwrap(),
             hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
             show_completions_on_input: defaults.show_completions_on_input.unwrap(),
             projects_online_by_default: defaults.projects_online_by_default.unwrap(),
@@ -302,7 +305,6 @@ impl Settings {
             editor_defaults: EditorSettings {
                 tab_size: required(defaults.editor.tab_size),
                 hard_tabs: required(defaults.editor.hard_tabs),
-                cursor_blink: required(defaults.editor.cursor_blink),
                 soft_wrap: required(defaults.editor.soft_wrap),
                 preferred_line_length: required(defaults.editor.preferred_line_length),
                 format_on_save: required(defaults.editor.format_on_save),
@@ -348,6 +350,7 @@ impl Settings {
         );
         merge(&mut self.buffer_font_size, data.buffer_font_size);
         merge(&mut self.default_buffer_font_size, data.buffer_font_size);
+        merge(&mut self.cursor_blink, data.cursor_blink);
         merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
         merge(
             &mut self.show_completions_on_input,
@@ -392,10 +395,6 @@ impl Settings {
         self.language_setting(language, |settings| settings.hard_tabs)
     }
 
-    pub fn cursor_blink(&self, language: Option<&str>) -> bool {
-        self.language_setting(language, |settings| settings.cursor_blink)
-    }
-
     pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
         self.language_setting(language, |settings| settings.soft_wrap)
     }
@@ -442,6 +441,7 @@ impl Settings {
             buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
             buffer_font_size: 14.,
             default_buffer_font_size: 14.,
+            cursor_blink: true,
             hover_popover_enabled: true,
             show_completions_on_input: true,
             vim_mode: false,
@@ -450,7 +450,6 @@ impl Settings {
             editor_defaults: EditorSettings {
                 tab_size: Some(4.try_into().unwrap()),
                 hard_tabs: Some(false),
-                cursor_blink: Some(true),
                 soft_wrap: Some(SoftWrap::None),
                 preferred_line_length: Some(80),
                 format_on_save: Some(FormatOnSave::On),