Merge pull request #138 from zed-industries/adjust-font

Antonio Scandurra created

Add bindings to adjust buffer font size

Change summary

zed/src/editor.rs                      |   2 
zed/src/editor/display_map.rs          | 108 ++++++++++++---------------
zed/src/editor/display_map/wrap_map.rs |  71 +++++++++++++----
zed/src/editor/element.rs              |   9 +
zed/src/editor/movement.rs             |   2 
zed/src/lib.rs                         |  21 +++++
6 files changed, 129 insertions(+), 84 deletions(-)

Detailed changes

zed/src/editor.rs πŸ”—

@@ -348,7 +348,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let display_map =
-            cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.borrow().clone(), None, cx));
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.clone(), None, cx));
         cx.observe(&buffer, Self::on_buffer_changed).detach();
         cx.subscribe(&buffer, Self::on_buffer_event).detach();
         cx.observe(&display_map, Self::on_display_map_changed)

zed/src/editor/display_map.rs πŸ”—

@@ -5,6 +5,7 @@ mod wrap_map;
 use super::{buffer, Anchor, Bias, Buffer, Point, Settings, ToOffset, ToPoint};
 use fold_map::FoldMap;
 use gpui::{Entity, ModelContext, ModelHandle};
+use postage::watch;
 use std::ops::Range;
 use tab_map::TabMap;
 pub use wrap_map::BufferRows;
@@ -24,12 +25,12 @@ impl Entity for DisplayMap {
 impl DisplayMap {
     pub fn new(
         buffer: ModelHandle<Buffer>,
-        settings: Settings,
+        settings: watch::Receiver<Settings>,
         wrap_width: Option<f32>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
-        let (tab_map, snapshot) = TabMap::new(snapshot, settings.tab_size);
+        let (tab_map, snapshot) = TabMap::new(snapshot, settings.borrow().tab_size);
         let wrap_map = cx.add_model(|cx| WrapMap::new(snapshot, settings, wrap_width, cx));
         cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
         DisplayMap {
@@ -387,6 +388,7 @@ mod tests {
             let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
             Buffer::new(0, text, cx)
         });
+        let settings = watch::channel_with(settings).1;
 
         let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings, wrap_width, cx));
         let (_observer, notifications) = Observer::new(&map, &mut cx);
@@ -543,7 +545,8 @@ mod tests {
 
         let text = "one two three four five\nsix seven eight";
         let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings, wrap_width, cx));
+        let (mut settings_tx, settings_rx) = watch::channel_with(settings);
+        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings_rx, wrap_width, cx));
 
         let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
         assert_eq!(
@@ -599,23 +602,28 @@ mod tests {
             snapshot.chunks_at(1).collect::<String>(),
             "three four \nfive\nsix and \nseven eight"
         );
+
+        // Re-wrap on font size changes
+        settings_tx.borrow_mut().buffer_font_size += 3.;
+
+        map.next_notification(&mut cx).await;
+
+        let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+        assert_eq!(
+            snapshot.chunks_at(1).collect::<String>(),
+            "three \nfour five\nsix and \nseven \neight"
+        )
     }
 
     #[gpui::test]
     fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
         let text = sample_text(6, 6);
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(
-                buffer.clone(),
-                Settings {
-                    tab_size: 4,
-                    ..Settings::test(cx)
-                },
-                None,
-                cx,
-            )
+        let settings = watch::channel_with(Settings {
+            tab_size: 4,
+            ..Settings::test(cx)
         });
+        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         buffer.update(cx, |buffer, cx| {
             buffer.edit(
                 vec![
@@ -687,17 +695,13 @@ mod tests {
         });
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
 
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(
-                buffer,
-                Settings {
-                    tab_size: 2,
-                    ..Settings::test(cx)
-                },
-                None,
-                cx,
-            )
+        let settings = cx.update(|cx| {
+            watch::channel_with(Settings {
+                tab_size: 2,
+                ..Settings::test(cx)
+            })
         });
+        let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, None, cx));
         assert_eq!(
             cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
             vec![
@@ -778,13 +782,15 @@ mod tests {
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
 
         let font_cache = cx.font_cache();
-        let settings = Settings {
-            tab_size: 4,
-            buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
-            buffer_font_size: 16.0,
-            ..cx.read(Settings::test)
-        };
-        let map = cx.add_model(|cx| DisplayMap::new(buffer, settings, Some(40.0), cx));
+        let settings = cx.update(|cx| {
+            watch::channel_with(Settings {
+                tab_size: 4,
+                buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
+                buffer_font_size: 16.0,
+                ..Settings::test(cx)
+            })
+        });
+        let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, Some(40.0), cx));
         assert_eq!(
             cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
             [
@@ -819,17 +825,11 @@ mod tests {
         let text = "\n'a', 'Ξ±',\t'βœ‹',\t'❎', '🍐'\n";
         let display_text = "\n'a', 'Ξ±',   'βœ‹',    '❎', '🍐'\n";
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(
-                buffer.clone(),
-                Settings {
-                    tab_size: 4,
-                    ..Settings::test(cx)
-                },
-                None,
-                cx,
-            )
+        let settings = watch::channel_with(Settings {
+            tab_size: 4,
+            ..Settings::test(cx)
         });
+        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         let map = map.update(cx, |map, cx| map.snapshot(cx));
 
         assert_eq!(map.text(), display_text);
@@ -863,17 +863,11 @@ mod tests {
     fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
         let text = "βœ…\t\tΞ±\nΞ²\t\nπŸ€Ξ²\t\tΞ³";
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(
-                buffer.clone(),
-                Settings {
-                    tab_size: 4,
-                    ..Settings::test(cx)
-                },
-                None,
-                cx,
-            )
+        let settings = watch::channel_with(Settings {
+            tab_size: 4,
+            ..Settings::test(cx)
         });
+        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         let map = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(map.text(), "βœ…       Ξ±\nΞ²   \nπŸ€Ξ²      Ξ³");
         assert_eq!(
@@ -930,17 +924,11 @@ mod tests {
     #[gpui::test]
     fn test_max_point(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(
-                buffer.clone(),
-                Settings {
-                    tab_size: 4,
-                    ..Settings::test(cx)
-                },
-                None,
-                cx,
-            )
+        let settings = watch::channel_with(Settings {
+            tab_size: 4,
+            ..Settings::test(cx)
         });
+        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         assert_eq!(
             map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
             DisplayPoint::new(1, 11)

zed/src/editor/display_map/wrap_map.rs πŸ”—

@@ -9,6 +9,7 @@ use gpui::{
     Entity, ModelContext, Task,
 };
 use lazy_static::lazy_static;
+use postage::{prelude::Stream, watch};
 use smol::future::yield_now;
 use std::{collections::VecDeque, ops::Range, time::Duration};
 
@@ -17,7 +18,8 @@ pub struct WrapMap {
     pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
     wrap_width: Option<f32>,
     background_task: Option<Task<()>>,
-    settings: Settings,
+    _watch_settings: Task<()>,
+    settings: watch::Receiver<Settings>,
 }
 
 impl Entity for WrapMap {
@@ -74,18 +76,39 @@ pub struct BufferRows<'a> {
 impl WrapMap {
     pub fn new(
         tab_snapshot: TabSnapshot,
-        settings: Settings,
+        settings: watch::Receiver<Settings>,
         wrap_width: Option<f32>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
+        let _watch_settings = cx.spawn_weak({
+            let mut prev_font = (
+                settings.borrow().buffer_font_size,
+                settings.borrow().buffer_font_family,
+            );
+            let mut settings = settings.clone();
+            move |this, mut cx| async move {
+                while let Some(settings) = settings.recv().await {
+                    if let Some(this) = this.upgrade(&cx) {
+                        let font = (settings.buffer_font_size, settings.buffer_font_family);
+                        if font != prev_font {
+                            prev_font = font;
+                            this.update(&mut cx, |this, cx| this.rewrap(cx));
+                        }
+                    }
+                }
+            }
+        });
+
         let mut this = Self {
-            background_task: None,
             wrap_width: None,
             pending_edits: Default::default(),
             snapshot: Snapshot::new(tab_snapshot),
             settings,
+            background_task: None,
+            _watch_settings,
         };
         this.set_wrap_width(wrap_width, cx);
+
         this
     }
 
@@ -111,17 +134,25 @@ impl WrapMap {
         }
 
         self.wrap_width = wrap_width;
+        self.rewrap(cx);
+        true
+    }
+
+    fn rewrap(&mut self, cx: &mut ModelContext<Self>) {
         self.background_task.take();
 
-        if let Some(wrap_width) = wrap_width {
+        if let Some(wrap_width) = self.wrap_width {
             let mut new_snapshot = self.snapshot.clone();
             let font_cache = cx.font_cache().clone();
             let settings = self.settings.clone();
             let task = cx.background().spawn(async move {
-                let font_id = font_cache
-                    .select_font(settings.buffer_font_family, &Default::default())
-                    .unwrap();
-                let mut line_wrapper = font_cache.line_wrapper(font_id, settings.buffer_font_size);
+                let mut line_wrapper = {
+                    let settings = settings.borrow();
+                    let font_id = font_cache
+                        .select_font(settings.buffer_font_family, &Default::default())
+                        .unwrap();
+                    font_cache.line_wrapper(font_id, settings.buffer_font_size)
+                };
                 let tab_snapshot = new_snapshot.tab_snapshot.clone();
                 let range = TabPoint::zero()..tab_snapshot.max_point();
                 new_snapshot
@@ -144,6 +175,7 @@ impl WrapMap {
             {
                 Ok(snapshot) => {
                     self.snapshot = snapshot;
+                    cx.notify();
                 }
                 Err(wrap_task) => {
                     self.background_task = Some(cx.spawn(|this, mut cx| async move {
@@ -166,8 +198,6 @@ impl WrapMap {
                     .push(Transform::isomorphic(summary), &());
             }
         }
-
-        true
     }
 
     fn flush_edits(&mut self, cx: &mut ModelContext<Self>) {
@@ -194,11 +224,14 @@ impl WrapMap {
                 let font_cache = cx.font_cache().clone();
                 let settings = self.settings.clone();
                 let update_task = cx.background().spawn(async move {
-                    let font_id = font_cache
-                        .select_font(settings.buffer_font_family, &Default::default())
-                        .unwrap();
-                    let mut line_wrapper =
-                        font_cache.line_wrapper(font_id, settings.buffer_font_size);
+                    let mut line_wrapper = {
+                        let settings = settings.borrow();
+                        let font_id = font_cache
+                            .select_font(settings.buffer_font_family, &Default::default())
+                            .unwrap();
+                        font_cache.line_wrapper(font_id, settings.buffer_font_size)
+                    };
+
                     for (tab_snapshot, edits) in pending_edits {
                         snapshot
                             .update(tab_snapshot, &edits, wrap_width, &mut line_wrapper)
@@ -942,9 +975,6 @@ mod tests {
             folds_snapshot.text()
         );
         log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
-        let wrap_map = cx
-            .add_model(|cx| WrapMap::new(tabs_snapshot.clone(), settings.clone(), wrap_width, cx));
-        let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
 
         let font_id = font_cache
             .select_font(settings.buffer_font_family, &Default::default())
@@ -953,6 +983,11 @@ mod tests {
         let unwrapped_text = tabs_snapshot.text();
         let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
 
+        let settings = watch::channel_with(settings).1;
+        let wrap_map = cx
+            .add_model(|cx| WrapMap::new(tabs_snapshot.clone(), settings.clone(), wrap_width, cx));
+        let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
+
         if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
             notifications.recv().await.unwrap();
         }

zed/src/editor/element.rs πŸ”—

@@ -8,6 +8,7 @@ use gpui::{
         PathBuilder,
     },
     json::{self, ToJson},
+    keymap::Keystroke,
     text_layout::{self, TextLayoutCache},
     AppContext, Axis, Border, Element, Event, EventContext, FontCache, LayoutContext,
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
@@ -127,14 +128,14 @@ impl EditorElement {
         }
     }
 
-    fn key_down(&self, chars: &str, cx: &mut EventContext) -> bool {
+    fn key_down(&self, chars: &str, keystroke: &Keystroke, cx: &mut EventContext) -> bool {
         let view = self.view.upgrade(cx.app).unwrap();
 
         if view.is_focused(cx.app) {
             if chars.is_empty() {
                 false
             } else {
-                if chars.chars().any(|c| c.is_control()) {
+                if chars.chars().any(|c| c.is_control()) || keystroke.cmd || keystroke.ctrl {
                     false
                 } else {
                     cx.dispatch_action(Insert(chars.to_string()));
@@ -629,7 +630,9 @@ impl Element for EditorElement {
                     delta,
                     precise,
                 } => self.scroll(*position, *delta, *precise, layout, paint, cx),
-                Event::KeyDown { chars, .. } => self.key_down(chars, cx),
+                Event::KeyDown {
+                    chars, keystroke, ..
+                } => self.key_down(chars, keystroke, cx),
                 _ => false,
             }
         } else {

zed/src/editor/movement.rs πŸ”—

@@ -187,7 +187,7 @@ mod tests {
 
     #[gpui::test]
     fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
-        let settings = test_app_state(cx).settings.borrow().clone();
+        let settings = test_app_state(cx).settings.clone();
         let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΞ” defΞ³", cx));
         let display_map = cx.add_model(|cx| DisplayMap::new(buffer, settings, None, cx));
         let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));

zed/src/lib.rs πŸ”—

@@ -22,7 +22,7 @@ pub mod worktree;
 
 use crate::util::TryFutureExt;
 use channel::ChannelList;
-use gpui::{action, ModelHandle};
+use gpui::{action, keymap::Binding, ModelHandle};
 use parking_lot::Mutex;
 use postage::watch;
 use std::sync::Arc;
@@ -32,6 +32,9 @@ pub use settings::Settings;
 action!(About);
 action!(Quit);
 action!(Authenticate);
+action!(AdjustBufferFontSize, f32);
+
+const MIN_FONT_SIZE: f32 = 6.0;
 
 pub struct AppState {
     pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
@@ -54,6 +57,22 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
                 .detach();
         }
     });
+
+    cx.add_global_action({
+        let settings_tx = app_state.settings_tx.clone();
+
+        move |action: &AdjustBufferFontSize, cx| {
+            let mut settings_tx = settings_tx.lock();
+            let new_size = (settings_tx.borrow().buffer_font_size + action.0).max(MIN_FONT_SIZE);
+            settings_tx.borrow_mut().buffer_font_size = new_size;
+            cx.refresh_windows();
+        }
+    });
+
+    cx.add_bindings(vec![
+        Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
+        Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
+    ])
 }
 
 fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {