Merge branch 'modified-status-in-tabs'

Max Brunsfeld created

Change summary

Cargo.lock                          |   1 
gpui/examples/text.rs               |   2 
gpui/src/elements/canvas.rs         |  73 ++++++++++++++
gpui/src/elements/container.rs      |   5 
gpui/src/elements/mod.rs            |   2 
gpui/src/platform/mac/window.rs     |   6 -
zed/Cargo.toml                      |   1 
zed/src/editor/buffer/mod.rs        | 161 ++++++++++++++++++++++++++----
zed/src/editor/buffer_view.rs       |  37 +++---
zed/src/editor/display_map/mod.rs   |   1 
zed/src/file_finder.rs              |   2 
zed/src/workspace/pane.rs           |  49 +++++++++
zed/src/workspace/workspace_view.rs |  50 ++++++---
13 files changed, 320 insertions(+), 70 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2241,6 +2241,7 @@ dependencies = [
  "crossbeam-channel 0.5.0",
  "dirs",
  "easy-parallel",
+ "futures-core",
  "gpui",
  "ignore",
  "lazy_static",

gpui/examples/text.rs 🔗

@@ -31,7 +31,7 @@ impl gpui::View for TextView {
         "View"
     }
 
-    fn render<'a>(&self, app: &gpui::AppContext) -> gpui::ElementBox {
+    fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox {
         TextElement.boxed()
     }
 }

gpui/src/elements/canvas.rs 🔗

@@ -0,0 +1,73 @@
+use super::Element;
+use crate::PaintContext;
+use pathfinder_geometry::{
+    rect::RectF,
+    vector::{vec2f, Vector2F},
+};
+
+pub struct Canvas<F>(F)
+where
+    F: FnMut(RectF, &mut PaintContext);
+
+impl<F> Canvas<F>
+where
+    F: FnMut(RectF, &mut PaintContext),
+{
+    pub fn new(f: F) -> Self {
+        Self(f)
+    }
+}
+
+impl<F> Element for Canvas<F>
+where
+    F: FnMut(RectF, &mut PaintContext),
+{
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: crate::SizeConstraint,
+        _: &mut crate::LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        let x = if constraint.max.x().is_finite() {
+            constraint.max.x()
+        } else {
+            constraint.min.x()
+        };
+        let y = if constraint.max.y().is_finite() {
+            constraint.max.y()
+        } else {
+            constraint.min.y()
+        };
+        (vec2f(x, y), ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        _: &mut Self::LayoutState,
+        ctx: &mut PaintContext,
+    ) -> Self::PaintState {
+        self.0(bounds, ctx)
+    }
+
+    fn after_layout(
+        &mut self,
+        _: Vector2F,
+        _: &mut Self::LayoutState,
+        _: &mut crate::AfterLayoutContext,
+    ) {
+    }
+
+    fn dispatch_event(
+        &mut self,
+        _: &crate::Event,
+        _: RectF,
+        _: &mut Self::LayoutState,
+        _: &mut Self::PaintState,
+        _: &mut crate::EventContext,
+    ) -> bool {
+        false
+    }
+}

gpui/src/elements/container.rs 🔗

@@ -36,6 +36,11 @@ impl Container {
         self
     }
 
+    pub fn with_margin_left(mut self, margin: f32) -> Self {
+        self.margin.left = margin;
+        self
+    }
+
     pub fn with_uniform_padding(mut self, padding: f32) -> Self {
         self.padding = Padding {
             top: padding,

gpui/src/elements/mod.rs 🔗

@@ -1,4 +1,5 @@
 mod align;
+mod canvas;
 mod constrained_box;
 mod container;
 mod empty;
@@ -13,6 +14,7 @@ mod uniform_list;
 
 pub use crate::presenter::ChildView;
 pub use align::*;
+pub use canvas::*;
 pub use constrained_box::*;
 pub use container::*;
 pub use empty::*;

gpui/src/platform/mac/window.rs 🔗

@@ -230,12 +230,6 @@ impl Window {
             Ok(window)
         }
     }
-
-    pub fn zoom(&self) {
-        unsafe {
-            self.0.as_ref().borrow().native_window.performZoom_(nil);
-        }
-    }
 }
 
 impl Drop for Window {

zed/Cargo.toml 🔗

@@ -20,6 +20,7 @@ dirs = "3.0"
 easy-parallel = "3.1.0"
 gpui = {path = "../gpui"}
 ignore = {git = "https://github.com/zed-industries/ripgrep", rev = "1d152118f35b3e3590216709b86277062d79b8a0"}
+futures-core = "0.3"
 lazy_static = "1.4.0"
 libc = "0.2"
 log = "0.4"

zed/src/editor/buffer/mod.rs 🔗

@@ -3,6 +3,7 @@ mod point;
 mod text;
 
 pub use anchor::*;
+use futures_core::future::LocalBoxFuture;
 pub use point::*;
 pub use text::*;
 
@@ -14,7 +15,7 @@ use crate::{
     worktree::FileHandle,
 };
 use anyhow::{anyhow, Result};
-use gpui::{AppContext, Entity, ModelContext, Task};
+use gpui::{AppContext, Entity, ModelContext};
 use lazy_static::lazy_static;
 use rand::prelude::*;
 use std::{
@@ -36,6 +37,7 @@ pub struct Buffer {
     fragments: SumTree<Fragment>,
     insertion_splits: HashMap<time::Local, SumTree<InsertionSplit>>,
     pub version: time::Global,
+    saved_version: time::Global,
     last_edit: time::Local,
     selections: HashMap<SelectionSetId, Vec<Selection>>,
     pub selections_last_update: SelectionsVersion,
@@ -216,6 +218,7 @@ impl Buffer {
             fragments,
             insertion_splits,
             version: time::Global::new(),
+            saved_version: time::Global::new(),
             last_edit: time::Local::default(),
             selections: HashMap::default(),
             selections_last_update: 0,
@@ -241,17 +244,34 @@ impl Buffer {
         }
     }
 
-    pub fn save(&self, ctx: &mut ModelContext<Self>) -> Option<Task<Result<()>>> {
+    pub fn save(&mut self, ctx: &mut ModelContext<Self>) -> LocalBoxFuture<'static, Result<()>> {
         if let Some(file) = &self.file {
             let snapshot = self.snapshot();
-            Some(file.save(snapshot, ctx.app()))
+            let version = self.version.clone();
+            let save_task = file.save(snapshot, ctx.app());
+            let task = ctx.spawn(save_task, |me, save_result, ctx| {
+                if save_result.is_ok() {
+                    me.did_save(version, ctx);
+                }
+                save_result
+            });
+            Box::pin(task)
         } else {
-            None
+            Box::pin(async { Ok(()) })
         }
     }
 
-    pub fn is_modified(&self) -> bool {
-        self.version != time::Global::new()
+    fn did_save(&mut self, version: time::Global, ctx: &mut ModelContext<Buffer>) {
+        self.saved_version = version;
+        ctx.emit(Event::Saved);
+    }
+
+    pub fn is_dirty(&self) -> bool {
+        self.version > self.saved_version
+    }
+
+    pub fn version(&self) -> time::Global {
+        self.version.clone()
     }
 
     pub fn text_summary(&self) -> TextSummary {
@@ -398,6 +418,7 @@ impl Buffer {
             None
         };
 
+        let was_dirty = self.is_dirty();
         let old_version = self.version.clone();
         let old_ranges = old_ranges
             .into_iter()
@@ -416,7 +437,7 @@ impl Buffer {
                 ctx.notify();
                 let changes = self.edits_since(old_version).collect::<Vec<_>>();
                 if !changes.is_empty() {
-                    ctx.emit(Event::Edited(changes))
+                    self.did_edit(changes, was_dirty, ctx);
                 }
             }
 
@@ -434,6 +455,13 @@ impl Buffer {
         Ok(ops)
     }
 
+    fn did_edit(&self, changes: Vec<Edit>, was_dirty: bool, ctx: &mut ModelContext<Self>) {
+        ctx.emit(Event::Edited(changes));
+        if !was_dirty {
+            ctx.emit(Event::Dirtied);
+        }
+    }
+
     pub fn simulate_typing<T: Rng>(&mut self, rng: &mut T) {
         let end = rng.gen_range(0..self.len() + 1);
         let start = rng.gen_range(0..end + 1);
@@ -619,6 +647,7 @@ impl Buffer {
         ops: I,
         ctx: Option<&mut ModelContext<Self>>,
     ) -> Result<()> {
+        let was_dirty = self.is_dirty();
         let old_version = self.version.clone();
 
         let mut deferred_ops = Vec::new();
@@ -637,7 +666,7 @@ impl Buffer {
             ctx.notify();
             let changes = self.edits_since(old_version).collect::<Vec<_>>();
             if !changes.is_empty() {
-                ctx.emit(Event::Edited(changes));
+                self.did_edit(changes, was_dirty, ctx);
             }
         }
 
@@ -1370,6 +1399,7 @@ impl Clone for Buffer {
             fragments: self.fragments.clone(),
             insertion_splits: self.insertion_splits.clone(),
             version: self.version.clone(),
+            saved_version: self.saved_version.clone(),
             last_edit: self.last_edit.clone(),
             selections: self.selections.clone(),
             selections_last_update: self.selections_last_update.clone(),
@@ -1395,6 +1425,8 @@ impl Snapshot {
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum Event {
     Edited(Vec<Edit>),
+    Dirtied,
+    Saved,
 }
 
 impl Entity for Buffer {
@@ -1948,7 +1980,9 @@ impl ToPoint for usize {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use gpui::App;
     use std::collections::BTreeMap;
+    use std::{cell::RefCell, rc::Rc};
 
     #[test]
     fn test_edit() -> Result<()> {
@@ -1970,9 +2004,6 @@ mod tests {
 
     #[test]
     fn test_edit_events() {
-        use gpui::App;
-        use std::{cell::RefCell, rc::Rc};
-
         App::test((), |mut app| async move {
             let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
             let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
@@ -1998,19 +2029,25 @@ mod tests {
             let buffer_1_events = buffer_1_events.borrow();
             assert_eq!(
                 *buffer_1_events,
-                vec![Event::Edited(vec![Edit {
-                    old_range: 2..4,
-                    new_range: 2..5
-                }])]
+                vec![
+                    Event::Edited(vec![Edit {
+                        old_range: 2..4,
+                        new_range: 2..5
+                    },]),
+                    Event::Dirtied
+                ]
             );
 
             let buffer_2_events = buffer_2_events.borrow();
             assert_eq!(
                 *buffer_2_events,
-                vec![Event::Edited(vec![Edit {
-                    old_range: 2..4,
-                    new_range: 2..5
-                }])]
+                vec![
+                    Event::Edited(vec![Edit {
+                        old_range: 2..4,
+                        new_range: 2..5
+                    },]),
+                    Event::Dirtied
+                ]
             );
         });
     }
@@ -2484,11 +2521,89 @@ mod tests {
 
     #[test]
     fn test_is_modified() -> Result<()> {
-        let mut buffer = Buffer::new(0, "abc");
-        assert!(!buffer.is_modified());
-        buffer.edit(vec![1..2], "", None)?;
-        assert!(buffer.is_modified());
+        App::test((), |mut app| async move {
+            let model = app.add_model(|_| Buffer::new(0, "abc"));
+            let events = Rc::new(RefCell::new(Vec::new()));
+
+            // initially, the buffer isn't dirty.
+            model.update(&mut app, |buffer, ctx| {
+                ctx.subscribe(&model, {
+                    let events = events.clone();
+                    move |_, event, _| events.borrow_mut().push(event.clone())
+                });
 
+                assert!(!buffer.is_dirty());
+                assert!(events.borrow().is_empty());
+
+                buffer.edit(vec![1..2], "", Some(ctx)).unwrap();
+            });
+
+            // after the first edit, the buffer is dirty, and emits a dirtied event.
+            model.update(&mut app, |buffer, ctx| {
+                assert!(buffer.text() == "ac");
+                assert!(buffer.is_dirty());
+                assert_eq!(
+                    *events.borrow(),
+                    &[
+                        Event::Edited(vec![Edit {
+                            old_range: 1..2,
+                            new_range: 1..1
+                        }]),
+                        Event::Dirtied
+                    ]
+                );
+                events.borrow_mut().clear();
+
+                buffer.did_save(buffer.version(), ctx);
+            });
+
+            // after saving, the buffer is not dirty, and emits a saved event.
+            model.update(&mut app, |buffer, ctx| {
+                assert!(!buffer.is_dirty());
+                assert_eq!(*events.borrow(), &[Event::Saved]);
+                events.borrow_mut().clear();
+
+                buffer.edit(vec![1..1], "B", Some(ctx)).unwrap();
+                buffer.edit(vec![2..2], "D", Some(ctx)).unwrap();
+            });
+
+            // after editing again, the buffer is dirty, and emits another dirty event.
+            model.update(&mut app, |buffer, ctx| {
+                assert!(buffer.text() == "aBDc");
+                assert!(buffer.is_dirty());
+                assert_eq!(
+                    *events.borrow(),
+                    &[
+                        Event::Edited(vec![Edit {
+                            old_range: 1..1,
+                            new_range: 1..2
+                        }]),
+                        Event::Dirtied,
+                        Event::Edited(vec![Edit {
+                            old_range: 2..2,
+                            new_range: 2..3
+                        }]),
+                    ],
+                );
+                events.borrow_mut().clear();
+
+                // TODO - currently, after restoring the buffer to its
+                // previously-saved state, the is still considered dirty.
+                buffer.edit(vec![1..3], "", Some(ctx)).unwrap();
+                assert!(buffer.text() == "ac");
+                assert!(buffer.is_dirty());
+            });
+
+            model.update(&mut app, |_, _| {
+                assert_eq!(
+                    *events.borrow(),
+                    &[Event::Edited(vec![Edit {
+                        old_range: 1..3,
+                        new_range: 1..1
+                    },])]
+                );
+            });
+        });
         Ok(())
     }
 

zed/src/editor/buffer_view.rs 🔗

@@ -4,10 +4,10 @@ use super::{
 };
 use crate::{settings::Settings, watch, workspace};
 use anyhow::Result;
+use futures_core::future::LocalBoxFuture;
 use gpui::{
     fonts::Properties as FontProperties, keymap::Binding, text_layout, App, AppContext, Element,
-    ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, Task, View, ViewContext,
-    WeakViewHandle,
+    ElementBox, Entity, FontCache, ModelHandle, View, ViewContext, WeakViewHandle,
 };
 use gpui::{geometry::vector::Vector2F, TextLayoutCache};
 use parking_lot::Mutex;
@@ -82,18 +82,6 @@ pub enum SelectAction {
     End,
 }
 
-// impl workspace::Item for Buffer {
-//     type View = BufferView;
-
-//     fn build_view(
-//         buffer: ModelHandle<Self>,
-//         settings: watch::Receiver<Settings>,
-//         ctx: &mut ViewContext<Self::View>,
-//     ) -> Self::View {
-//         BufferView::for_buffer(buffer, settings, ctx)
-//     }
-// }
-
 pub struct BufferView {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<Buffer>,
@@ -1091,6 +1079,8 @@ impl BufferView {
     ) {
         match event {
             buffer::Event::Edited(_) => ctx.emit(Event::Edited),
+            buffer::Event::Dirtied => ctx.emit(Event::Dirtied),
+            buffer::Event::Saved => ctx.emit(Event::Saved),
         }
     }
 }
@@ -1106,6 +1096,8 @@ pub enum Event {
     Activate,
     Edited,
     Blurred,
+    Dirtied,
+    Saved,
 }
 
 impl Entity for BufferView {
@@ -1147,11 +1139,12 @@ impl workspace::Item for Buffer {
 }
 
 impl workspace::ItemView for BufferView {
-    fn is_activate_event(event: &Self::Event) -> bool {
-        match event {
-            Event::Activate => true,
-            _ => false,
-        }
+    fn should_activate_item_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Activate)
+    }
+
+    fn should_update_tab_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Saved | Event::Dirtied)
     }
 
     fn title(&self, app: &AppContext) -> std::string::String {
@@ -1178,9 +1171,13 @@ impl workspace::ItemView for BufferView {
         Some(clone)
     }
 
-    fn save(&self, ctx: &mut MutableAppContext) -> Option<Task<Result<()>>> {
+    fn save(&self, ctx: &mut ViewContext<Self>) -> LocalBoxFuture<'static, Result<()>> {
         self.buffer.update(ctx, |buffer, ctx| buffer.save(ctx))
     }
+
+    fn is_dirty(&self, ctx: &AppContext) -> bool {
+        self.buffer.as_ref(ctx).is_dirty()
+    }
 }
 
 impl Selection {

zed/src/editor/display_map/mod.rs 🔗

@@ -126,6 +126,7 @@ impl DisplayMap {
     fn handle_buffer_event(&mut self, event: &buffer::Event, ctx: &mut ModelContext<Self>) {
         match event {
             buffer::Event::Edited(edits) => self.fold_map.apply_edits(edits, ctx.app()).unwrap(),
+            _ => {}
         }
     }
 }

zed/src/file_finder.rs 🔗

@@ -309,7 +309,7 @@ impl FileFinder {
                 }
             }
             Blurred => ctx.emit(Event::Dismissed),
-            Activate => {}
+            _ => {}
         }
     }
 

zed/src/workspace/pane.rs 🔗

@@ -1,7 +1,11 @@
 use super::{ItemViewHandle, SplitDirection};
 use crate::{settings::Settings, watch};
 use gpui::{
-    color::ColorU, elements::*, keymap::Binding, App, AppContext, Border, Entity, View, ViewContext,
+    color::{ColorF, ColorU},
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    keymap::Binding,
+    App, AppContext, Border, Entity, Quad, View, ViewContext,
 };
 use std::cmp;
 
@@ -190,7 +194,28 @@ impl Pane {
             let padding = 6.;
             let mut container = Container::new(
                 Align::new(
-                    Label::new(title, settings.ui_font_family, settings.ui_font_size).boxed(),
+                    Flex::row()
+                        .with_child(
+                            Label::new(title, settings.ui_font_family, settings.ui_font_size)
+                                .boxed(),
+                        )
+                        .with_child(
+                            Container::new(
+                                LineBox::new(
+                                    settings.ui_font_family,
+                                    settings.ui_font_size,
+                                    ConstrainedBox::new(Self::render_modified_icon(
+                                        item.is_dirty(app),
+                                    ))
+                                    .with_max_width(12.)
+                                    .boxed(),
+                                )
+                                .boxed(),
+                            )
+                            .with_margin_left(20.)
+                            .boxed(),
+                        )
+                        .boxed(),
                 )
                 .boxed(),
             )
@@ -243,6 +268,26 @@ impl Pane {
 
         row.boxed()
     }
+
+    fn render_modified_icon(is_modified: bool) -> ElementBox {
+        Canvas::new(move |bounds, ctx| {
+            if is_modified {
+                let padding = if bounds.height() < bounds.width() {
+                    vec2f(bounds.width() - bounds.height(), 0.0)
+                } else {
+                    vec2f(0.0, bounds.height() - bounds.width())
+                };
+                let square = RectF::new(bounds.origin() + padding / 2., bounds.size() - padding);
+                ctx.scene.push_quad(Quad {
+                    bounds: square,
+                    background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
+                    border: Default::default(),
+                    corner_radius: square.width() / 2.,
+                });
+            }
+        })
+        .boxed()
+    }
 }
 
 impl Entity for Pane {

zed/src/workspace/workspace_view.rs 🔗

@@ -1,8 +1,9 @@
 use super::{pane, Pane, PaneGroup, SplitDirection, Workspace};
 use crate::{settings::Settings, watch};
+use futures_core::future::LocalBoxFuture;
 use gpui::{
     color::rgbu, elements::*, keymap::Binding, AnyViewHandle, App, AppContext, Entity, ModelHandle,
-    MutableAppContext, Task, View, ViewContext, ViewHandle,
+    MutableAppContext, View, ViewContext, ViewHandle,
 };
 use log::{error, info};
 use std::{collections::HashSet, path::PathBuf};
@@ -13,7 +14,6 @@ pub fn init(app: &mut App) {
 }
 
 pub trait ItemView: View {
-    fn is_activate_event(event: &Self::Event) -> bool;
     fn title(&self, app: &AppContext) -> String;
     fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>;
     fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
@@ -22,8 +22,17 @@ pub trait ItemView: View {
     {
         None
     }
-    fn save(&self, _: &mut MutableAppContext) -> Option<Task<anyhow::Result<()>>> {
-        None
+    fn is_dirty(&self, _: &AppContext) -> bool {
+        false
+    }
+    fn save(&self, _: &mut ViewContext<Self>) -> LocalBoxFuture<'static, anyhow::Result<()>> {
+        Box::pin(async { Ok(()) })
+    }
+    fn should_activate_item_on_event(_: &Self::Event) -> bool {
+        false
+    }
+    fn should_update_tab_on_event(_: &Self::Event) -> bool {
+        false
     }
 }
 
@@ -35,7 +44,8 @@ pub trait ItemViewHandle: Send + Sync {
     fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
     fn id(&self) -> usize;
     fn to_any(&self) -> AnyViewHandle;
-    fn save(&self, ctx: &mut MutableAppContext) -> Option<Task<anyhow::Result<()>>>;
+    fn is_dirty(&self, ctx: &AppContext) -> bool;
+    fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>>;
 }
 
 impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
@@ -61,18 +71,25 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
     fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext) {
         pane.update(app, |_, ctx| {
             ctx.subscribe_to_view(self, |pane, item, event, ctx| {
-                if T::is_activate_event(event) {
+                if T::should_activate_item_on_event(event) {
                     if let Some(ix) = pane.item_index(&item) {
                         pane.activate_item(ix, ctx);
                         pane.activate(ctx);
                     }
                 }
+                if T::should_update_tab_on_event(event) {
+                    ctx.notify()
+                }
             })
         })
     }
 
-    fn save(&self, ctx: &mut MutableAppContext) -> Option<Task<anyhow::Result<()>>> {
-        self.update(ctx, |item, ctx| item.save(ctx.app_mut()))
+    fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>> {
+        self.update(ctx, |item, ctx| item.save(ctx))
+    }
+
+    fn is_dirty(&self, ctx: &AppContext) -> bool {
+        self.as_ref(ctx).is_dirty(ctx)
     }
 
     fn id(&self) -> usize {
@@ -222,15 +239,14 @@ impl WorkspaceView {
     pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
         self.active_pane.update(ctx, |pane, ctx| {
             if let Some(item) = pane.active_item() {
-                if let Some(task) = item.save(ctx.app_mut()) {
-                    ctx.spawn(task, |_, result, _| {
-                        if let Err(e) = result {
-                            // TODO - present this error to the user
-                            error!("failed to save item: {:?}, ", e);
-                        }
-                    })
-                    .detach();
-                }
+                let task = item.save(ctx.app_mut());
+                ctx.spawn(task, |_, result, _| {
+                    if let Err(e) = result {
+                        // TODO - present this error to the user
+                        error!("failed to save item: {:?}, ", e);
+                    }
+                })
+                .detach()
             }
         });
     }