Simplify `ListState` API (#35685)

Mikayla Maki and Agus Zubiaga created

Follow up to: https://github.com/zed-industries/zed/pull/35670,
simplifies the List state APIs so you no longer have to worry about
strong vs. weak pointers when rendering list items.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>

Change summary

crates/agent_ui/src/acp/thread_view.rs                       |  37 
crates/agent_ui/src/active_thread.rs                         |  17 
crates/agent_ui/src/agent_panel.rs                           |   9 
crates/collab_ui/src/chat_panel.rs                           |  39 
crates/collab_ui/src/collab_panel.rs                         |  24 
crates/collab_ui/src/notification_panel.rs                   |  22 
crates/debugger_ui/src/session/running/loaded_source_list.rs |  24 
crates/debugger_ui/src/session/running/stack_frame_list.rs   |  21 
crates/gpui/src/elements/list.rs                             |  84 +-
crates/markdown_preview/src/markdown_preview_view.rs         | 217 ++---
crates/markdown_preview/src/markdown_renderer.rs             |  28 
crates/picker/src/picker.rs                                  |  39 
crates/repl/src/notebook/notebook_ui.rs                      |  38 
crates/semantic_index/src/project_index_debug_view.rs        |  22 
crates/zed/src/zed/component_preview.rs                      | 102 -
15 files changed, 321 insertions(+), 402 deletions(-)

Detailed changes

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -173,23 +173,7 @@ impl AcpThreadView {
 
         let mention_set = mention_set.clone();
 
-        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0), {
-            let this = cx.entity().downgrade();
-            move |index: usize, window, cx| {
-                let Some(this) = this.upgrade() else {
-                    return Empty.into_any();
-                };
-                this.update(cx, |this, cx| {
-                    let Some((entry, len)) = this.thread().and_then(|thread| {
-                        let entries = &thread.read(cx).entries();
-                        Some((entries.get(index)?, entries.len()))
-                    }) else {
-                        return Empty.into_any();
-                    };
-                    this.render_entry(index, len, entry, window, cx)
-                })
-            }
-        });
+        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 
         Self {
             agent: agent.clone(),
@@ -2552,10 +2536,21 @@ impl Render for AcpThreadView {
                     v_flex().flex_1().map(|this| {
                         if self.list_state.item_count() > 0 {
                             this.child(
-                                list(self.list_state.clone())
-                                    .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
-                                    .flex_grow()
-                                    .into_any(),
+                                list(
+                                    self.list_state.clone(),
+                                    cx.processor(|this, index: usize, window, cx| {
+                                        let Some((entry, len)) = this.thread().and_then(|thread| {
+                                            let entries = &thread.read(cx).entries();
+                                            Some((entries.get(index)?, entries.len()))
+                                        }) else {
+                                            return Empty.into_any();
+                                        };
+                                        this.render_entry(index, len, entry, window, cx)
+                                    }),
+                                )
+                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
+                                .flex_grow()
+                                .into_any(),
                             )
                             .children(match thread_clone.read(cx).status() {
                                 ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => {

crates/agent_ui/src/active_thread.rs 🔗

@@ -780,13 +780,7 @@ impl ActiveThread {
             cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
         ];
 
-        let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
-            let this = cx.entity().downgrade();
-            move |ix, window: &mut Window, cx: &mut App| {
-                this.update(cx, |this, cx| this.render_message(ix, window, cx))
-                    .unwrap()
-            }
-        });
+        let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.));
 
         let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
             Some(cx.observe_release(&workspace, |this, _, cx| {
@@ -1846,7 +1840,12 @@ impl ActiveThread {
             )))
     }
 
-    fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
+    fn render_message(
+        &mut self,
+        ix: usize,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> AnyElement {
         let message_id = self.messages[ix];
         let workspace = self.workspace.clone();
         let thread = self.thread.read(cx);
@@ -3613,7 +3612,7 @@ impl Render for ActiveThread {
                     this.hide_scrollbar_later(cx);
                 }),
             )
-            .child(list(self.list_state.clone()).flex_grow())
+            .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow())
             .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
                 this.child(scrollbar)
             })

crates/agent_ui/src/agent_panel.rs 🔗

@@ -1471,7 +1471,6 @@ impl AgentPanel {
 
         let current_is_special = current_is_history || current_is_config;
         let new_is_special = new_is_history || new_is_config;
-        let mut old_acp_thread = None;
 
         match &self.active_view {
             ActiveView::Thread { thread, .. } => {
@@ -1483,9 +1482,6 @@ impl AgentPanel {
                     });
                 }
             }
-            ActiveView::ExternalAgentThread { thread_view } => {
-                old_acp_thread.replace(thread_view.downgrade());
-            }
             _ => {}
         }
 
@@ -1516,11 +1512,6 @@ impl AgentPanel {
             self.active_view = new_view;
         }
 
-        debug_assert!(
-            old_acp_thread.map_or(true, |thread| !thread.is_upgradable()),
-            "AcpThreadView leaked"
-        );
-
         self.acp_message_history.borrow_mut().reset_position();
 
         self.focus_handle(cx).focus(window);

crates/collab_ui/src/chat_panel.rs 🔗

@@ -103,28 +103,16 @@ impl ChatPanel {
         });
 
         cx.new(|cx| {
-            let entity = cx.entity().downgrade();
-            let message_list = ListState::new(
-                0,
-                gpui::ListAlignment::Bottom,
-                px(1000.),
-                move |ix, window, cx| {
-                    if let Some(entity) = entity.upgrade() {
-                        entity.update(cx, |this: &mut Self, cx| {
-                            this.render_message(ix, window, cx).into_any_element()
-                        })
-                    } else {
-                        div().into_any()
+            let message_list = ListState::new(0, gpui::ListAlignment::Bottom, px(1000.));
+
+            message_list.set_scroll_handler(cx.listener(
+                |this: &mut Self, event: &ListScrollEvent, _, cx| {
+                    if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
+                        this.load_more_messages(cx);
                     }
+                    this.is_scrolled_to_bottom = !event.is_scrolled;
                 },
-            );
-
-            message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, _, cx| {
-                if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
-                    this.load_more_messages(cx);
-                }
-                this.is_scrolled_to_bottom = !event.is_scrolled;
-            }));
+            ));
 
             let local_offset = chrono::Local::now().offset().local_minus_utc();
             let mut this = Self {
@@ -399,7 +387,7 @@ impl ChatPanel {
         ix: usize,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> impl IntoElement {
+    ) -> AnyElement {
         let active_chat = &self.active_chat.as_ref().unwrap().0;
         let (message, is_continuation_from_previous, is_admin) =
             active_chat.update(cx, |active_chat, cx| {
@@ -582,6 +570,7 @@ impl ChatPanel {
                 self.render_popover_buttons(message_id, can_delete_message, can_edit_message, cx)
                     .mt_neg_2p5(),
             )
+            .into_any_element()
     }
 
     fn has_open_menu(&self, message_id: Option<u64>) -> bool {
@@ -979,7 +968,13 @@ impl Render for ChatPanel {
             )
             .child(div().flex_grow().px_2().map(|this| {
                 if self.active_chat.is_some() {
-                    this.child(list(self.message_list.clone()).size_full())
+                    this.child(
+                        list(
+                            self.message_list.clone(),
+                            cx.processor(Self::render_message),
+                        )
+                        .size_full(),
+                    )
                 } else {
                     this.child(
                         div()

crates/collab_ui/src/collab_panel.rs 🔗

@@ -324,20 +324,6 @@ impl CollabPanel {
             )
             .detach();
 
-            let entity = cx.entity().downgrade();
-            let list_state = ListState::new(
-                0,
-                gpui::ListAlignment::Top,
-                px(1000.),
-                move |ix, window, cx| {
-                    if let Some(entity) = entity.upgrade() {
-                        entity.update(cx, |this, cx| this.render_list_entry(ix, window, cx))
-                    } else {
-                        div().into_any()
-                    }
-                },
-            );
-
             let mut this = Self {
                 width: None,
                 focus_handle: cx.focus_handle(),
@@ -345,7 +331,7 @@ impl CollabPanel {
                 fs: workspace.app_state().fs.clone(),
                 pending_serialization: Task::ready(None),
                 context_menu: None,
-                list_state,
+                list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
                 channel_name_editor,
                 filter_editor,
                 entries: Vec::default(),
@@ -2431,7 +2417,13 @@ impl CollabPanel {
         });
         v_flex()
             .size_full()
-            .child(list(self.list_state.clone()).size_full())
+            .child(
+                list(
+                    self.list_state.clone(),
+                    cx.processor(Self::render_list_entry),
+                )
+                .size_full(),
+            )
             .child(
                 v_flex()
                     .child(div().mx_2().border_primary(cx).border_t_1())

crates/collab_ui/src/notification_panel.rs 🔗

@@ -118,16 +118,7 @@ impl NotificationPanel {
             })
             .detach();
 
-            let entity = cx.entity().downgrade();
-            let notification_list =
-                ListState::new(0, ListAlignment::Top, px(1000.), move |ix, window, cx| {
-                    entity
-                        .upgrade()
-                        .and_then(|entity| {
-                            entity.update(cx, |this, cx| this.render_notification(ix, window, cx))
-                        })
-                        .unwrap_or_else(|| div().into_any())
-                });
+            let notification_list = ListState::new(0, ListAlignment::Top, px(1000.));
             notification_list.set_scroll_handler(cx.listener(
                 |this, event: &ListScrollEvent, _, cx| {
                     if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
@@ -687,7 +678,16 @@ impl Render for NotificationPanel {
                         ),
                     )
                 } else {
-                    this.child(list(self.notification_list.clone()).size_full())
+                    this.child(
+                        list(
+                            self.notification_list.clone(),
+                            cx.processor(|this, ix, window, cx| {
+                                this.render_notification(ix, window, cx)
+                                    .unwrap_or_else(|| div().into_any())
+                            }),
+                        )
+                        .size_full(),
+                    )
                 }
             })
     }

crates/debugger_ui/src/session/running/loaded_source_list.rs 🔗

@@ -13,22 +13,8 @@ pub(crate) struct LoadedSourceList {
 
 impl LoadedSourceList {
     pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self {
-        let weak_entity = cx.weak_entity();
         let focus_handle = cx.focus_handle();
-
-        let list = ListState::new(
-            0,
-            gpui::ListAlignment::Top,
-            px(1000.),
-            move |ix, _window, cx| {
-                weak_entity
-                    .upgrade()
-                    .map(|loaded_sources| {
-                        loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx))
-                    })
-                    .unwrap_or(div().into_any())
-            },
-        );
+        let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
 
         let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
             SessionEvent::Stopped(_) | SessionEvent::LoadedSources => {
@@ -98,6 +84,12 @@ impl Render for LoadedSourceList {
             .track_focus(&self.focus_handle)
             .size_full()
             .p_1()
-            .child(list(self.list.clone()).size_full())
+            .child(
+                list(
+                    self.list.clone(),
+                    cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
+                )
+                .size_full(),
+            )
     }
 }

crates/debugger_ui/src/session/running/stack_frame_list.rs 🔗

@@ -70,13 +70,7 @@ impl StackFrameList {
                 _ => {}
             });
 
-        let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), {
-            let this = cx.weak_entity();
-            move |ix, _window, cx| {
-                this.update(cx, |this, cx| this.render_entry(ix, cx))
-                    .unwrap_or(div().into_any())
-            }
-        });
+        let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
         let scrollbar_state = ScrollbarState::new(list_state.clone());
 
         let mut this = Self {
@@ -708,11 +702,14 @@ impl StackFrameList {
         self.activate_selected_entry(window, cx);
     }
 
-    fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        div()
-            .p_1()
-            .size_full()
-            .child(list(self.list_state.clone()).size_full())
+    fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        div().p_1().size_full().child(
+            list(
+                self.list_state.clone(),
+                cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
+            )
+            .size_full(),
+        )
     }
 }
 

crates/gpui/src/elements/list.rs 🔗

@@ -18,10 +18,16 @@ use refineable::Refineable as _;
 use std::{cell::RefCell, ops::Range, rc::Rc};
 use sum_tree::{Bias, Dimensions, SumTree};
 
+type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static;
+
 /// Construct a new list element
-pub fn list(state: ListState) -> List {
+pub fn list(
+    state: ListState,
+    render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
+) -> List {
     List {
         state,
+        render_item: Box::new(render_item),
         style: StyleRefinement::default(),
         sizing_behavior: ListSizingBehavior::default(),
     }
@@ -30,6 +36,7 @@ pub fn list(state: ListState) -> List {
 /// A list element
 pub struct List {
     state: ListState,
+    render_item: Box<RenderItemFn>,
     style: StyleRefinement,
     sizing_behavior: ListSizingBehavior,
 }
@@ -55,7 +62,6 @@ impl std::fmt::Debug for ListState {
 struct StateInner {
     last_layout_bounds: Option<Bounds<Pixels>>,
     last_padding: Option<Edges<Pixels>>,
-    render_item: Box<dyn FnMut(usize, &mut Window, &mut App) -> AnyElement>,
     items: SumTree<ListItem>,
     logical_scroll_top: Option<ListOffset>,
     alignment: ListAlignment,
@@ -186,19 +192,10 @@ impl ListState {
     /// above and below the visible area. Elements within this area will
     /// be measured even though they are not visible. This can help ensure
     /// that the list doesn't flicker or pop in when scrolling.
-    pub fn new<R>(
-        item_count: usize,
-        alignment: ListAlignment,
-        overdraw: Pixels,
-        render_item: R,
-    ) -> Self
-    where
-        R: 'static + FnMut(usize, &mut Window, &mut App) -> AnyElement,
-    {
+    pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
         let this = Self(Rc::new(RefCell::new(StateInner {
             last_layout_bounds: None,
             last_padding: None,
-            render_item: Box::new(render_item),
             items: SumTree::default(),
             logical_scroll_top: None,
             alignment,
@@ -532,6 +529,7 @@ impl StateInner {
         available_width: Option<Pixels>,
         available_height: Pixels,
         padding: &Edges<Pixels>,
+        render_item: &mut RenderItemFn,
         window: &mut Window,
         cx: &mut App,
     ) -> LayoutItemsResponse {
@@ -566,7 +564,7 @@ impl StateInner {
             // If we're within the visible area or the height wasn't cached, render and measure the item's element
             if visible_height < available_height || size.is_none() {
                 let item_index = scroll_top.item_ix + ix;
-                let mut element = (self.render_item)(item_index, window, cx);
+                let mut element = render_item(item_index, window, cx);
                 let element_size = element.layout_as_root(available_item_space, window, cx);
                 size = Some(element_size);
                 if visible_height < available_height {
@@ -601,7 +599,7 @@ impl StateInner {
                 cursor.prev();
                 if let Some(item) = cursor.item() {
                     let item_index = cursor.start().0;
-                    let mut element = (self.render_item)(item_index, window, cx);
+                    let mut element = render_item(item_index, window, cx);
                     let element_size = element.layout_as_root(available_item_space, window, cx);
                     let focus_handle = item.focus_handle();
                     rendered_height += element_size.height;
@@ -650,7 +648,7 @@ impl StateInner {
                 let size = if let ListItem::Measured { size, .. } = item {
                     *size
                 } else {
-                    let mut element = (self.render_item)(cursor.start().0, window, cx);
+                    let mut element = render_item(cursor.start().0, window, cx);
                     element.layout_as_root(available_item_space, window, cx)
                 };
 
@@ -683,7 +681,7 @@ impl StateInner {
             while let Some(item) = cursor.item() {
                 if item.contains_focused(window, cx) {
                     let item_index = cursor.start().0;
-                    let mut element = (self.render_item)(cursor.start().0, window, cx);
+                    let mut element = render_item(cursor.start().0, window, cx);
                     let size = element.layout_as_root(available_item_space, window, cx);
                     item_layouts.push_back(ItemLayout {
                         index: item_index,
@@ -708,6 +706,7 @@ impl StateInner {
         bounds: Bounds<Pixels>,
         padding: Edges<Pixels>,
         autoscroll: bool,
+        render_item: &mut RenderItemFn,
         window: &mut Window,
         cx: &mut App,
     ) -> Result<LayoutItemsResponse, ListOffset> {
@@ -716,6 +715,7 @@ impl StateInner {
                 Some(bounds.size.width),
                 bounds.size.height,
                 &padding,
+                render_item,
                 window,
                 cx,
             );
@@ -753,8 +753,7 @@ impl StateInner {
                                     let Some(item) = cursor.item() else { break };
 
                                     let size = item.size().unwrap_or_else(|| {
-                                        let mut item =
-                                            (self.render_item)(cursor.start().0, window, cx);
+                                        let mut item = render_item(cursor.start().0, window, cx);
                                         let item_available_size = size(
                                             bounds.size.width.into(),
                                             AvailableSpace::MinContent,
@@ -876,8 +875,14 @@ impl Element for List {
                         window.rem_size(),
                     );
 
-                    let layout_response =
-                        state.layout_items(None, available_height, &padding, window, cx);
+                    let layout_response = state.layout_items(
+                        None,
+                        available_height,
+                        &padding,
+                        &mut self.render_item,
+                        window,
+                        cx,
+                    );
                     let max_element_width = layout_response.max_item_width;
 
                     let summary = state.items.summary();
@@ -951,15 +956,16 @@ impl Element for List {
         let padding = style
             .padding
             .to_pixels(bounds.size.into(), window.rem_size());
-        let layout = match state.prepaint_items(bounds, padding, true, window, cx) {
-            Ok(layout) => layout,
-            Err(autoscroll_request) => {
-                state.logical_scroll_top = Some(autoscroll_request);
-                state
-                    .prepaint_items(bounds, padding, false, window, cx)
-                    .unwrap()
-            }
-        };
+        let layout =
+            match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
+                Ok(layout) => layout,
+                Err(autoscroll_request) => {
+                    state.logical_scroll_top = Some(autoscroll_request);
+                    state
+                        .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
+                        .unwrap()
+                }
+            };
 
         state.last_layout_bounds = Some(bounds);
         state.last_padding = Some(padding);
@@ -1108,9 +1114,7 @@ mod test {
 
         let cx = cx.add_empty_window();
 
-        let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| {
-            div().h(px(10.)).w_full().into_any()
-        });
+        let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
 
         // Ensure that the list is scrolled to the top
         state.scroll_to(gpui::ListOffset {
@@ -1121,7 +1125,11 @@ mod test {
         struct TestView(ListState);
         impl Render for TestView {
             fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
-                list(self.0.clone()).w_full().h_full()
+                list(self.0.clone(), |_, _, _| {
+                    div().h(px(10.)).w_full().into_any()
+                })
+                .w_full()
+                .h_full()
             }
         }
 
@@ -1154,14 +1162,16 @@ mod test {
 
         let cx = cx.add_empty_window();
 
-        let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| {
-            div().h(px(20.)).w_full().into_any()
-        });
+        let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
 
         struct TestView(ListState);
         impl Render for TestView {
             fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
-                list(self.0.clone()).w_full().h_full()
+                list(self.0.clone(), |_, _, _| {
+                    div().h(px(20.)).w_full().into_any()
+                })
+                .w_full()
+                .h_full()
             }
         }
 

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -18,6 +18,7 @@ use workspace::item::{Item, ItemHandle};
 use workspace::{Pane, Workspace};
 
 use crate::markdown_elements::ParsedMarkdownElement;
+use crate::markdown_renderer::CheckboxClickedEvent;
 use crate::{
     MovePageDown, MovePageUp, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide,
     markdown_elements::ParsedMarkdown,
@@ -203,114 +204,7 @@ impl MarkdownPreviewView {
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
         cx.new(|cx| {
-            let view = cx.entity().downgrade();
-
-            let list_state = ListState::new(
-                0,
-                gpui::ListAlignment::Top,
-                px(1000.),
-                move |ix, window, cx| {
-                    if let Some(view) = view.upgrade() {
-                        view.update(cx, |this: &mut Self, cx| {
-                            let Some(contents) = &this.contents else {
-                                return div().into_any();
-                            };
-
-                            let mut render_cx =
-                                RenderContext::new(Some(this.workspace.clone()), window, cx)
-                                    .with_checkbox_clicked_callback({
-                                        let view = view.clone();
-                                        move |checked, source_range, window, cx| {
-                                            view.update(cx, |view, cx| {
-                                                if let Some(editor) = view
-                                                    .active_editor
-                                                    .as_ref()
-                                                    .map(|s| s.editor.clone())
-                                                {
-                                                    editor.update(cx, |editor, cx| {
-                                                        let task_marker =
-                                                            if checked { "[x]" } else { "[ ]" };
-
-                                                        editor.edit(
-                                                            vec![(source_range, task_marker)],
-                                                            cx,
-                                                        );
-                                                    });
-                                                    view.parse_markdown_from_active_editor(
-                                                        false, window, cx,
-                                                    );
-                                                    cx.notify();
-                                                }
-                                            })
-                                        }
-                                    });
-
-                            let block = contents.children.get(ix).unwrap();
-                            let rendered_block = render_markdown_block(block, &mut render_cx);
-
-                            let should_apply_padding = Self::should_apply_padding_between(
-                                block,
-                                contents.children.get(ix + 1),
-                            );
-
-                            div()
-                                .id(ix)
-                                .when(should_apply_padding, |this| {
-                                    this.pb(render_cx.scaled_rems(0.75))
-                                })
-                                .group("markdown-block")
-                                .on_click(cx.listener(
-                                    move |this, event: &ClickEvent, window, cx| {
-                                        if event.click_count() == 2 {
-                                            if let Some(source_range) = this
-                                                .contents
-                                                .as_ref()
-                                                .and_then(|c| c.children.get(ix))
-                                                .and_then(|block| block.source_range())
-                                            {
-                                                this.move_cursor_to_block(
-                                                    window,
-                                                    cx,
-                                                    source_range.start..source_range.start,
-                                                );
-                                            }
-                                        }
-                                    },
-                                ))
-                                .map(move |container| {
-                                    let indicator = div()
-                                        .h_full()
-                                        .w(px(4.0))
-                                        .when(ix == this.selected_block, |this| {
-                                            this.bg(cx.theme().colors().border)
-                                        })
-                                        .group_hover("markdown-block", |s| {
-                                            if ix == this.selected_block {
-                                                s
-                                            } else {
-                                                s.bg(cx.theme().colors().border_variant)
-                                            }
-                                        })
-                                        .rounded_xs();
-
-                                    container.child(
-                                        div()
-                                            .relative()
-                                            .child(
-                                                div()
-                                                    .pl(render_cx.scaled_rems(1.0))
-                                                    .child(rendered_block),
-                                            )
-                                            .child(indicator.absolute().left_0().top_0()),
-                                    )
-                                })
-                                .into_any()
-                        })
-                    } else {
-                        div().into_any()
-                    }
-                },
-            );
+            let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
 
             let mut this = Self {
                 selected_block: 0,
@@ -607,10 +501,107 @@ impl Render for MarkdownPreviewView {
             .p_4()
             .text_size(buffer_size)
             .line_height(relative(buffer_line_height.value()))
-            .child(
-                div()
-                    .flex_grow()
-                    .map(|this| this.child(list(self.list_state.clone()).size_full())),
-            )
+            .child(div().flex_grow().map(|this| {
+                this.child(
+                    list(
+                        self.list_state.clone(),
+                        cx.processor(|this, ix, window, cx| {
+                            let Some(contents) = &this.contents else {
+                                return div().into_any();
+                            };
+
+                            let mut render_cx =
+                                RenderContext::new(Some(this.workspace.clone()), window, cx)
+                                    .with_checkbox_clicked_callback(cx.listener(
+                                        move |this, e: &CheckboxClickedEvent, window, cx| {
+                                            if let Some(editor) = this
+                                                .active_editor
+                                                .as_ref()
+                                                .map(|s| s.editor.clone())
+                                            {
+                                                editor.update(cx, |editor, cx| {
+                                                    let task_marker =
+                                                        if e.checked() { "[x]" } else { "[ ]" };
+
+                                                    editor.edit(
+                                                        vec![(e.source_range(), task_marker)],
+                                                        cx,
+                                                    );
+                                                });
+                                                this.parse_markdown_from_active_editor(
+                                                    false, window, cx,
+                                                );
+                                                cx.notify();
+                                            }
+                                        },
+                                    ));
+
+                            let block = contents.children.get(ix).unwrap();
+                            let rendered_block = render_markdown_block(block, &mut render_cx);
+
+                            let should_apply_padding = Self::should_apply_padding_between(
+                                block,
+                                contents.children.get(ix + 1),
+                            );
+
+                            div()
+                                .id(ix)
+                                .when(should_apply_padding, |this| {
+                                    this.pb(render_cx.scaled_rems(0.75))
+                                })
+                                .group("markdown-block")
+                                .on_click(cx.listener(
+                                    move |this, event: &ClickEvent, window, cx| {
+                                        if event.click_count() == 2 {
+                                            if let Some(source_range) = this
+                                                .contents
+                                                .as_ref()
+                                                .and_then(|c| c.children.get(ix))
+                                                .and_then(|block: &ParsedMarkdownElement| {
+                                                    block.source_range()
+                                                })
+                                            {
+                                                this.move_cursor_to_block(
+                                                    window,
+                                                    cx,
+                                                    source_range.start..source_range.start,
+                                                );
+                                            }
+                                        }
+                                    },
+                                ))
+                                .map(move |container| {
+                                    let indicator = div()
+                                        .h_full()
+                                        .w(px(4.0))
+                                        .when(ix == this.selected_block, |this| {
+                                            this.bg(cx.theme().colors().border)
+                                        })
+                                        .group_hover("markdown-block", |s| {
+                                            if ix == this.selected_block {
+                                                s
+                                            } else {
+                                                s.bg(cx.theme().colors().border_variant)
+                                            }
+                                        })
+                                        .rounded_xs();
+
+                                    container.child(
+                                        div()
+                                            .relative()
+                                            .child(
+                                                div()
+                                                    .pl(render_cx.scaled_rems(1.0))
+                                                    .child(rendered_block),
+                                            )
+                                            .child(indicator.absolute().left_0().top_0()),
+                                    )
+                                })
+                                .into_any()
+                        }),
+                    )
+                    .size_full(),
+                )
+            }))
     }
 }

crates/markdown_preview/src/markdown_renderer.rs 🔗

@@ -26,7 +26,22 @@ use ui::{
 };
 use workspace::{OpenOptions, OpenVisible, Workspace};
 
-type CheckboxClickedCallback = Arc<Box<dyn Fn(bool, Range<usize>, &mut Window, &mut App)>>;
+pub struct CheckboxClickedEvent {
+    pub checked: bool,
+    pub source_range: Range<usize>,
+}
+
+impl CheckboxClickedEvent {
+    pub fn source_range(&self) -> Range<usize> {
+        self.source_range.clone()
+    }
+
+    pub fn checked(&self) -> bool {
+        self.checked
+    }
+}
+
+type CheckboxClickedCallback = Arc<Box<dyn Fn(&CheckboxClickedEvent, &mut Window, &mut App)>>;
 
 #[derive(Clone)]
 pub struct RenderContext {
@@ -80,7 +95,7 @@ impl RenderContext {
 
     pub fn with_checkbox_clicked_callback(
         mut self,
-        callback: impl Fn(bool, Range<usize>, &mut Window, &mut App) + 'static,
+        callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static,
     ) -> Self {
         self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback)));
         self
@@ -229,7 +244,14 @@ fn render_markdown_list_item(
                                 };
 
                                 if window.modifiers().secondary() {
-                                    callback(checked, range.clone(), window, cx);
+                                    callback(
+                                        &CheckboxClickedEvent {
+                                            checked,
+                                            source_range: range.clone(),
+                                        },
+                                        window,
+                                        cx,
+                                    );
                                 }
                             }
                         })

crates/picker/src/picker.rs 🔗

@@ -292,7 +292,7 @@ impl<D: PickerDelegate> Picker<D> {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let element_container = Self::create_element_container(container, cx);
+        let element_container = Self::create_element_container(container);
         let scrollbar_state = match &element_container {
             ElementContainer::UniformList(scroll_handle) => {
                 ScrollbarState::new(scroll_handle.clone())
@@ -323,31 +323,13 @@ impl<D: PickerDelegate> Picker<D> {
         this
     }
 
-    fn create_element_container(
-        container: ContainerKind,
-        cx: &mut Context<Self>,
-    ) -> ElementContainer {
+    fn create_element_container(container: ContainerKind) -> ElementContainer {
         match container {
             ContainerKind::UniformList => {
                 ElementContainer::UniformList(UniformListScrollHandle::new())
             }
             ContainerKind::List => {
-                let entity = cx.entity().downgrade();
-                ElementContainer::List(ListState::new(
-                    0,
-                    gpui::ListAlignment::Top,
-                    px(1000.),
-                    move |ix, window, cx| {
-                        entity
-                            .upgrade()
-                            .map(|entity| {
-                                entity.update(cx, |this, cx| {
-                                    this.render_element(window, cx, ix).into_any_element()
-                                })
-                            })
-                            .unwrap_or_else(|| div().into_any_element())
-                    },
-                ))
+                ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.)))
             }
         }
     }
@@ -786,11 +768,16 @@ impl<D: PickerDelegate> Picker<D> {
             .py_1()
             .track_scroll(scroll_handle.clone())
             .into_any_element(),
-            ElementContainer::List(state) => list(state.clone())
-                .with_sizing_behavior(sizing_behavior)
-                .flex_grow()
-                .py_2()
-                .into_any_element(),
+            ElementContainer::List(state) => list(
+                state.clone(),
+                cx.processor(|this, ix, window, cx| {
+                    this.render_element(window, cx, ix).into_any_element()
+                }),
+            )
+            .with_sizing_behavior(sizing_behavior)
+            .flex_grow()
+            .py_2()
+            .into_any_element(),
         }
     }
 

crates/repl/src/notebook/notebook_ui.rs 🔗

@@ -126,29 +126,7 @@ impl NotebookEditor {
         let cell_count = cell_order.len();
 
         let this = cx.entity();
-        let cell_list = ListState::new(
-            cell_count,
-            gpui::ListAlignment::Top,
-            px(1000.),
-            move |ix, window, cx| {
-                notebook_handle
-                    .upgrade()
-                    .and_then(|notebook_handle| {
-                        notebook_handle.update(cx, |notebook, cx| {
-                            notebook
-                                .cell_order
-                                .get(ix)
-                                .and_then(|cell_id| notebook.cell_map.get(cell_id))
-                                .map(|cell| {
-                                    notebook
-                                        .render_cell(ix, cell, window, cx)
-                                        .into_any_element()
-                                })
-                        })
-                    })
-                    .unwrap_or_else(|| div().into_any())
-            },
-        );
+        let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.));
 
         Self {
             project,
@@ -544,7 +522,19 @@ impl Render for NotebookEditor {
                     .flex_1()
                     .size_full()
                     .overflow_y_scroll()
-                    .child(list(self.cell_list.clone()).size_full()),
+                    .child(list(
+                        self.cell_list.clone(),
+                        cx.processor(|this, ix, window, cx| {
+                            this.cell_order
+                                .get(ix)
+                                .and_then(|cell_id| this.cell_map.get(cell_id))
+                                .map(|cell| {
+                                    this.render_cell(ix, cell, window, cx).into_any_element()
+                                })
+                                .unwrap_or_else(|| div().into_any())
+                        }),
+                    ))
+                    .size_full(),
             )
             .child(self.render_notebook_controls(window, cx))
     }

crates/semantic_index/src/project_index_debug_view.rs 🔗

@@ -115,21 +115,9 @@ impl ProjectIndexDebugView {
                 .collect::<Vec<_>>();
 
             this.update(cx, |this, cx| {
-                let view = cx.entity().downgrade();
                 this.selected_path = Some(PathState {
                     path: file_path,
-                    list_state: ListState::new(
-                        chunks.len(),
-                        gpui::ListAlignment::Top,
-                        px(100.),
-                        move |ix, _, cx| {
-                            if let Some(view) = view.upgrade() {
-                                view.update(cx, |view, cx| view.render_chunk(ix, cx))
-                            } else {
-                                div().into_any()
-                            }
-                        },
-                    ),
+                    list_state: ListState::new(chunks.len(), gpui::ListAlignment::Top, px(100.)),
                     chunks,
                 });
                 cx.notify();
@@ -219,7 +207,13 @@ impl Render for ProjectIndexDebugView {
                             cx.notify();
                         })),
                 )
-                .child(list(selected_path.list_state.clone()).size_full())
+                .child(
+                    list(
+                        selected_path.list_state.clone(),
+                        cx.processor(|this, ix, _, cx| this.render_chunk(ix, cx)),
+                    )
+                    .size_full(),
+                )
                 .size_full()
                 .into_any_element()
         } else {

crates/zed/src/zed/component_preview.rs 🔗

@@ -107,6 +107,7 @@ struct ComponentPreview {
     active_thread: Option<Entity<ActiveThread>>,
     reset_key: usize,
     component_list: ListState,
+    entries: Vec<PreviewEntry>,
     component_map: HashMap<ComponentId, ComponentMetadata>,
     components: Vec<ComponentMetadata>,
     cursor_index: usize,
@@ -172,17 +173,6 @@ impl ComponentPreview {
             sorted_components.len(),
             gpui::ListAlignment::Top,
             px(1500.0),
-            {
-                let this = cx.entity().downgrade();
-                move |ix, window: &mut Window, cx: &mut App| {
-                    this.update(cx, |this, cx| {
-                        let component = this.get_component(ix);
-                        this.render_preview(&component, window, cx)
-                            .into_any_element()
-                    })
-                    .unwrap()
-                }
-            },
         );
 
         let mut component_preview = Self {
@@ -190,6 +180,7 @@ impl ComponentPreview {
             active_thread: None,
             reset_key: 0,
             component_list,
+            entries: Vec::new(),
             component_map: component_registry.component_map(),
             components: sorted_components,
             cursor_index: selected_index,
@@ -276,10 +267,6 @@ impl ComponentPreview {
         cx.notify();
     }
 
-    fn get_component(&self, ix: usize) -> ComponentMetadata {
-        self.components[ix].clone()
-    }
-
     fn filtered_components(&self) -> Vec<ComponentMetadata> {
         if self.filter_text.is_empty() {
             return self.components.clone();
@@ -420,7 +407,6 @@ impl ComponentPreview {
     fn update_component_list(&mut self, cx: &mut Context<Self>) {
         let entries = self.scope_ordered_entries();
         let new_len = entries.len();
-        let weak_entity = cx.entity().downgrade();
 
         if new_len > 0 {
             self.nav_scroll_handle
@@ -446,56 +432,9 @@ impl ComponentPreview {
             }
         }
 
-        self.component_list = ListState::new(
-            filtered_components.len(),
-            gpui::ListAlignment::Top,
-            px(1500.0),
-            {
-                let components = filtered_components.clone();
-                let this = cx.entity().downgrade();
-                move |ix, window: &mut Window, cx: &mut App| {
-                    if ix >= components.len() {
-                        return div().w_full().h_0().into_any_element();
-                    }
-
-                    this.update(cx, |this, cx| {
-                        let component = &components[ix];
-                        this.render_preview(component, window, cx)
-                            .into_any_element()
-                    })
-                    .unwrap()
-                }
-            },
-        );
+        self.component_list = ListState::new(new_len, gpui::ListAlignment::Top, px(1500.0));
+        self.entries = entries;
 
-        let new_list = ListState::new(
-            new_len,
-            gpui::ListAlignment::Top,
-            px(1500.0),
-            move |ix, window, cx| {
-                if ix >= entries.len() {
-                    return div().w_full().h_0().into_any_element();
-                }
-
-                let entry = &entries[ix];
-
-                weak_entity
-                    .update(cx, |this, cx| match entry {
-                        PreviewEntry::Component(component, _) => this
-                            .render_preview(component, window, cx)
-                            .into_any_element(),
-                        PreviewEntry::SectionHeader(shared_string) => this
-                            .render_scope_header(ix, shared_string.clone(), window, cx)
-                            .into_any_element(),
-                        PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
-                        PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
-                        PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
-                    })
-                    .unwrap()
-            },
-        );
-
-        self.component_list = new_list;
         cx.emit(ItemEvent::UpdateTab);
     }
 
@@ -672,10 +611,35 @@ impl ComponentPreview {
                         .child(format!("No components matching '{}'.", self.filter_text))
                         .into_any_element()
                 } else {
-                    list(self.component_list.clone())
-                        .flex_grow()
-                        .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
-                        .into_any_element()
+                    list(
+                        self.component_list.clone(),
+                        cx.processor(|this, ix, window, cx| {
+                            if ix >= this.entries.len() {
+                                return div().w_full().h_0().into_any_element();
+                            }
+
+                            let entry = &this.entries[ix];
+
+                            match entry {
+                                PreviewEntry::Component(component, _) => this
+                                    .render_preview(component, window, cx)
+                                    .into_any_element(),
+                                PreviewEntry::SectionHeader(shared_string) => this
+                                    .render_scope_header(ix, shared_string.clone(), window, cx)
+                                    .into_any_element(),
+                                PreviewEntry::AllComponents => {
+                                    div().w_full().h_0().into_any_element()
+                                }
+                                PreviewEntry::ActiveThread => {
+                                    div().w_full().h_0().into_any_element()
+                                }
+                                PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
+                            }
+                        }),
+                    )
+                    .flex_grow()
+                    .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
+                    .into_any_element()
                 },
             )
     }