Clean up inline assist editor rendering (#15536)

Antonio Scandurra , Nathan , and Max created

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Max <max@zed.dev>

Change summary

Cargo.lock                                    |    2 
crates/assistant/src/assistant_panel.rs       |   26 
crates/assistant/src/inline_assistant.rs      |   72 
crates/diagnostics/Cargo.toml                 |    2 
crates/diagnostics/src/diagnostics.rs         |    9 
crates/diagnostics/src/grouped_diagnostics.rs | 1403 ---------------------
crates/diagnostics/src/toolbar_controls.rs    |   85 
crates/editor/src/display_map.rs              |   61 
crates/editor/src/display_map/block_map.rs    |   96 
crates/editor/src/editor.rs                   |   40 
crates/editor/src/element.rs                  |  148 +
crates/editor/src/hunk_diff.rs                |   14 
crates/editor/src/scroll.rs                   |    4 
crates/editor/src/scroll/autoscroll.rs        |    4 
crates/feature_flags/src/feature_flags.rs     |    5 
crates/gpui/src/window.rs                     |    6 
crates/repl/src/outputs.rs                    |   37 
crates/repl/src/session.rs                    |   24 
crates/repl/src/stdio.rs                      |    4 
19 files changed, 266 insertions(+), 1,776 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3391,13 +3391,11 @@ dependencies = [
  "ctor",
  "editor",
  "env_logger",
- "feature_flags",
  "futures 0.3.28",
  "gpui",
  "language",
  "log",
  "lsp",
- "multi_buffer",
  "pretty_assertions",
  "project",
  "rand 0.8.5",

crates/assistant/src/assistant_panel.rs 🔗

@@ -1917,18 +1917,20 @@ impl ContextEditor {
                 cx.update(|cx| {
                     for suggestion in suggestion_group.suggestions {
                         let description = suggestion.description.unwrap_or_else(|| "Delete".into());
+
                         let range = {
-                            let buffer = editor.read(cx).buffer().read(cx).read(cx);
-                            let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
-                            buffer
+                            let multibuffer = editor.read(cx).buffer().read(cx).read(cx);
+                            let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
+                            multibuffer
                                 .anchor_in_excerpt(excerpt_id, suggestion.range.start)
                                 .unwrap()
-                                ..buffer
+                                ..multibuffer
                                     .anchor_in_excerpt(excerpt_id, suggestion.range.end)
                                     .unwrap()
                         };
+
                         InlineAssistant::update_global(cx, |assistant, cx| {
-                            assist_ids.push(assistant.suggest_assist(
+                            let suggestion_id = assistant.suggest_assist(
                                 &editor,
                                 range,
                                 description,
@@ -1936,16 +1938,20 @@ impl ContextEditor {
                                 Some(workspace.clone()),
                                 assistant_panel.upgrade().as_ref(),
                                 cx,
-                            ));
+                            );
+                            assist_ids.push(suggestion_id);
                         });
                     }
 
                     // Scroll the editor to the suggested assist
                     editor.update(cx, |editor, cx| {
-                        let anchor = {
-                            let buffer = editor.buffer().read(cx).read(cx);
-                            let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
-                            buffer
+                        let multibuffer = editor.buffer().read(cx).snapshot(cx);
+                        let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
+                        let anchor = if suggestion_group.context_range.start.to_offset(buffer) == 0
+                        {
+                            Anchor::min()
+                        } else {
+                            multibuffer
                                 .anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start)
                                 .unwrap()
                         };

crates/assistant/src/inline_assistant.rs 🔗

@@ -331,11 +331,16 @@ impl InlineAssistant {
         prompt_editor: &View<PromptEditor>,
         cx: &mut WindowContext,
     ) -> [CustomBlockId; 2] {
+        let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| {
+            prompt_editor
+                .editor
+                .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1 + 2)
+        });
         let assist_blocks = vec![
             BlockProperties {
                 style: BlockStyle::Sticky,
                 position: range.start,
-                height: prompt_editor.read(cx).height_in_lines,
+                height: prompt_editor_height,
                 render: build_assist_editor_renderer(prompt_editor),
                 disposition: BlockDisposition::Above,
             },
@@ -446,9 +451,6 @@ impl InlineAssistant {
             PromptEditorEvent::DismissRequested => {
                 self.dismiss_assist(assist_id, cx);
             }
-            PromptEditorEvent::Resized { height_in_lines } => {
-                self.resize_assist(assist_id, *height_in_lines, cx);
-            }
         }
     }
 
@@ -786,33 +788,6 @@ impl InlineAssistant {
         });
     }
 
-    fn resize_assist(
-        &mut self,
-        assist_id: InlineAssistId,
-        height_in_lines: u8,
-        cx: &mut WindowContext,
-    ) {
-        if let Some(assist) = self.assists.get_mut(&assist_id) {
-            if let Some(editor) = assist.editor.upgrade() {
-                if let Some(decorations) = assist.decorations.as_ref() {
-                    let mut new_blocks = HashMap::default();
-                    new_blocks.insert(
-                        decorations.prompt_block_id,
-                        (
-                            Some(height_in_lines),
-                            build_assist_editor_renderer(&decorations.prompt_editor),
-                        ),
-                    );
-                    editor.update(cx, |editor, cx| {
-                        editor
-                            .display_map
-                            .update(cx, |map, cx| map.replace_blocks(new_blocks, cx))
-                    });
-                }
-            }
-        }
-    }
-
     fn unlink_assist_group(
         &mut self,
         assist_group_id: InlineAssistGroupId,
@@ -1029,8 +1004,8 @@ impl InlineAssistant {
                     editor
                 });
 
-                let height = deleted_lines_editor
-                    .update(cx, |editor, cx| editor.max_point(cx).row().0 as u8 + 1);
+                let height =
+                    deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
                 new_blocks.push(BlockProperties {
                     position: new_row,
                     height,
@@ -1194,13 +1169,11 @@ enum PromptEditorEvent {
     ConfirmRequested,
     CancelRequested,
     DismissRequested,
-    Resized { height_in_lines: u8 },
 }
 
 struct PromptEditor {
     id: InlineAssistId,
     fs: Arc<dyn Fs>,
-    height_in_lines: u8,
     editor: View<Editor>,
     edited_since_done: bool,
     gutter_dimensions: Arc<Mutex<GutterDimensions>>,
@@ -1307,9 +1280,8 @@ impl Render for PromptEditor {
             .bg(cx.theme().colors().editor_background)
             .border_y_1()
             .border_color(cx.theme().status().info_border)
-            .py_1p5()
-            .h_full()
-            .w_full()
+            .size_full()
+            .py(cx.line_height() / 2.)
             .on_action(cx.listener(Self::confirm))
             .on_action(cx.listener(Self::cancel))
             .on_action(cx.listener(Self::move_up))
@@ -1427,7 +1399,6 @@ impl PromptEditor {
 
         let mut this = Self {
             id,
-            height_in_lines: 1,
             editor: prompt_editor,
             edited_since_done: false,
             gutter_dimensions,
@@ -1443,7 +1414,6 @@ impl PromptEditor {
             _token_count_subscriptions: token_count_subscriptions,
             workspace,
         };
-        this.count_lines(cx);
         this.count_tokens(cx);
         this.subscribe_to_editor(cx);
         this
@@ -1451,8 +1421,6 @@ impl PromptEditor {
 
     fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
         self.editor_subscriptions.clear();
-        self.editor_subscriptions
-            .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
         self.editor_subscriptions
             .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
     }
@@ -1487,22 +1455,6 @@ impl PromptEditor {
         self.editor.read(cx).text(cx)
     }
 
-    fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
-        let height_in_lines = cmp::max(
-            2, // Make the editor at least two lines tall, to account for padding and buttons.
-            cmp::min(
-                self.editor
-                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
-                Self::MAX_LINES as u32,
-            ),
-        ) as u8;
-
-        if height_in_lines != self.height_in_lines {
-            self.height_in_lines = height_in_lines;
-            cx.emit(PromptEditorEvent::Resized { height_in_lines });
-        }
-    }
-
     fn handle_parent_editor_event(
         &mut self,
         _: View<Editor>,
@@ -1545,10 +1497,6 @@ impl PromptEditor {
         })
     }
 
-    fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
-        self.count_lines(cx);
-    }
-
     fn handle_prompt_editor_events(
         &mut self,
         _: View<Editor>,

crates/diagnostics/Cargo.toml 🔗

@@ -18,13 +18,11 @@ collections.workspace = true
 ctor.workspace = true
 editor.workspace = true
 env_logger.workspace = true
-feature_flags.workspace = true
 futures.workspace = true
 gpui.workspace = true
 language.workspace = true
 log.workspace = true
 lsp.workspace = true
-multi_buffer.workspace = true
 project.workspace = true
 rand.workspace = true
 schemars.workspace = true

crates/diagnostics/src/diagnostics.rs 🔗

@@ -4,7 +4,6 @@ mod toolbar_controls;
 
 #[cfg(test)]
 mod diagnostics_tests;
-pub(crate) mod grouped_diagnostics;
 
 use anyhow::Result;
 use collections::{BTreeSet, HashSet};
@@ -15,7 +14,6 @@ use editor::{
     scroll::Autoscroll,
     Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
 };
-use feature_flags::FeatureFlagAppExt;
 use futures::{
     channel::mpsc::{self, UnboundedSender},
     StreamExt as _,
@@ -54,9 +52,6 @@ pub fn init(cx: &mut AppContext) {
     ProjectDiagnosticsSettings::register(cx);
     cx.observe_new_views(ProjectDiagnosticsEditor::register)
         .detach();
-    if !cx.has_flag::<feature_flags::GroupedDiagnostics>() {
-        grouped_diagnostics::init(cx);
-    }
 }
 
 struct ProjectDiagnosticsEditor {
@@ -469,7 +464,7 @@ impl ProjectDiagnosticsEditor {
                                     group_state.block_count += 1;
                                     blocks_to_add.push(BlockProperties {
                                         position: (excerpt_id, entry.range.start),
-                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
+                                        height: diagnostic.message.matches('\n').count() as u32 + 1,
                                         style: BlockStyle::Fixed,
                                         render: diagnostic_block_renderer(
                                             diagnostic, None, true, true,
@@ -787,7 +782,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
         let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
         h_flex()
             .id(DIAGNOSTIC_HEADER)
-            .py_2()
+            .h(2. * cx.line_height())
             .pl_10()
             .pr_5()
             .w_full()

crates/diagnostics/src/grouped_diagnostics.rs 🔗

@@ -1,1403 +0,0 @@
-use anyhow::Result;
-use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
-use editor::{
-    diagnostic_block_renderer,
-    display_map::{
-        BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId,
-        RenderBlock,
-    },
-    scroll::Autoscroll,
-    Bias, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToPoint,
-};
-use futures::{
-    channel::mpsc::{self, UnboundedSender},
-    StreamExt as _,
-};
-use gpui::{
-    actions, div, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
-    FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, SharedString,
-    Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
-};
-use language::{
-    Buffer, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, ToOffset,
-    ToPoint as _,
-};
-use lsp::LanguageServerId;
-use multi_buffer::{build_excerpt_ranges, ExpandExcerptDirection, MultiBufferRow};
-use project::{DiagnosticSummary, Project, ProjectPath};
-use settings::Settings;
-use std::{
-    any::{Any, TypeId},
-    cmp::Ordering,
-    ops::Range,
-    sync::{
-        atomic::{self, AtomicBool},
-        Arc,
-    },
-};
-use theme::ActiveTheme;
-use ui::{h_flex, prelude::*, Icon, IconName, Label};
-use util::{debug_panic, ResultExt};
-use workspace::{
-    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
-    ItemNavHistory, ToolbarItemLocation, Workspace,
-};
-
-use crate::project_diagnostics_settings::ProjectDiagnosticsSettings;
-actions!(grouped_diagnostics, [Deploy, ToggleWarnings]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(GroupedDiagnosticsEditor::register)
-        .detach();
-}
-
-pub struct GroupedDiagnosticsEditor {
-    pub project: Model<Project>,
-    workspace: WeakView<Workspace>,
-    focus_handle: FocusHandle,
-    editor: View<Editor>,
-    summary: DiagnosticSummary,
-    excerpts: Model<MultiBuffer>,
-    path_states: Vec<PathState>,
-    pub paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
-    pub include_warnings: bool,
-    context: u32,
-    pub update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>,
-    _update_excerpts_task: Task<Result<()>>,
-    _subscription: Subscription,
-}
-
-struct PathState {
-    path: ProjectPath,
-    first_excerpt_id: Option<ExcerptId>,
-    last_excerpt_id: Option<ExcerptId>,
-    diagnostics: Vec<(DiagnosticData, CustomBlockId)>,
-}
-
-#[derive(Debug, Clone)]
-struct DiagnosticData {
-    language_server_id: LanguageServerId,
-    is_primary: bool,
-    entry: DiagnosticEntry<language::Anchor>,
-}
-
-impl DiagnosticData {
-    fn diagnostic_entries_equal(&self, other: &DiagnosticData) -> bool {
-        self.language_server_id == other.language_server_id
-            && self.is_primary == other.is_primary
-            && self.entry.range == other.entry.range
-            && equal_without_group_ids(&self.entry.diagnostic, &other.entry.diagnostic)
-    }
-}
-
-// `group_id` can differ between LSP server diagnostics output,
-// hence ignore it when checking diagnostics for updates.
-fn equal_without_group_ids(a: &language::Diagnostic, b: &language::Diagnostic) -> bool {
-    a.source == b.source
-        && a.code == b.code
-        && a.severity == b.severity
-        && a.message == b.message
-        && a.is_primary == b.is_primary
-        && a.is_disk_based == b.is_disk_based
-        && a.is_unnecessary == b.is_unnecessary
-}
-
-impl EventEmitter<EditorEvent> for GroupedDiagnosticsEditor {}
-
-impl Render for GroupedDiagnosticsEditor {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let child = if self.path_states.is_empty() {
-            div()
-                .bg(cx.theme().colors().editor_background)
-                .flex()
-                .items_center()
-                .justify_center()
-                .size_full()
-                .child(Label::new("No problems in workspace"))
-        } else {
-            div().size_full().child(self.editor.clone())
-        };
-
-        div()
-            .track_focus(&self.focus_handle)
-            .when(self.path_states.is_empty(), |el| {
-                el.key_context("EmptyPane")
-            })
-            .size_full()
-            .on_action(cx.listener(Self::toggle_warnings))
-            .child(child)
-    }
-}
-
-impl GroupedDiagnosticsEditor {
-    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
-        workspace.register_action(Self::deploy);
-    }
-
-    fn new_with_context(
-        context: u32,
-        project_handle: Model<Project>,
-        workspace: WeakView<Workspace>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let project_event_subscription =
-            cx.subscribe(&project_handle, |this, project, event, cx| match event {
-                project::Event::DiskBasedDiagnosticsStarted { .. } => {
-                    cx.notify();
-                }
-                project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
-                    log::debug!("disk based diagnostics finished for server {language_server_id}");
-                    this.enqueue_update_stale_excerpts(Some(*language_server_id));
-                }
-                project::Event::DiagnosticsUpdated {
-                    language_server_id,
-                    path,
-                } => {
-                    this.paths_to_update
-                        .insert((path.clone(), *language_server_id));
-                    this.summary = project.read(cx).diagnostic_summary(false, cx);
-                    cx.emit(EditorEvent::TitleChanged);
-
-                    if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
-                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
-                    } else {
-                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
-                        this.enqueue_update_stale_excerpts(Some(*language_server_id));
-                    }
-                }
-                _ => {}
-            });
-
-        let focus_handle = cx.focus_handle();
-        cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))
-            .detach();
-        cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))
-            .detach();
-
-        let excerpts = cx.new_model(|cx| {
-            MultiBuffer::new(
-                project_handle.read(cx).replica_id(),
-                project_handle.read(cx).capability(),
-            )
-        });
-        let editor = cx.new_view(|cx| {
-            let mut editor =
-                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
-            editor.set_vertical_scroll_margin(5, cx);
-            editor
-        });
-        cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
-            cx.emit(event.clone());
-            match event {
-                EditorEvent::Focused => {
-                    if this.path_states.is_empty() {
-                        cx.focus(&this.focus_handle);
-                    }
-                }
-                EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None),
-                _ => {}
-            }
-        })
-        .detach();
-
-        let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded();
-
-        let project = project_handle.read(cx);
-        let mut this = Self {
-            project: project_handle.clone(),
-            context,
-            summary: project.diagnostic_summary(false, cx),
-            workspace,
-            excerpts,
-            focus_handle,
-            editor,
-            path_states: Vec::new(),
-            paths_to_update: BTreeSet::new(),
-            include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
-            update_paths_tx: update_excerpts_tx,
-            _update_excerpts_task: cx.spawn(move |this, mut cx| async move {
-                while let Some((path, language_server_id)) = update_excerpts_rx.next().await {
-                    if let Some(buffer) = project_handle
-                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
-                        .await
-                        .log_err()
-                    {
-                        this.update(&mut cx, |this, cx| {
-                            this.update_excerpts(path, language_server_id, buffer, cx);
-                        })?;
-                    }
-                }
-                anyhow::Ok(())
-            }),
-            _subscription: project_event_subscription,
-        };
-        this.enqueue_update_all_excerpts(cx);
-        this
-    }
-
-    fn new(
-        project_handle: Model<Project>,
-        workspace: WeakView<Workspace>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        Self::new_with_context(
-            editor::DEFAULT_MULTIBUFFER_CONTEXT,
-            project_handle,
-            workspace,
-            cx,
-        )
-    }
-
-    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
-        if let Some(existing) = workspace.item_of_type::<GroupedDiagnosticsEditor>(cx) {
-            workspace.activate_item(&existing, true, true, cx);
-        } else {
-            let workspace_handle = cx.view().downgrade();
-            let diagnostics = cx.new_view(|cx| {
-                GroupedDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
-            });
-            workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
-        }
-    }
-
-    pub fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
-        self.include_warnings = !self.include_warnings;
-        self.enqueue_update_all_excerpts(cx);
-        cx.notify();
-    }
-
-    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
-        if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
-            self.editor.focus_handle(cx).focus(cx)
-        }
-    }
-
-    fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
-        if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {
-            self.enqueue_update_stale_excerpts(None);
-        }
-    }
-
-    /// Enqueue an update of all excerpts. Updates all paths that either
-    /// currently have diagnostics or are currently present in this view.
-    fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
-        self.project.update(cx, |project, cx| {
-            let mut paths = project
-                .diagnostic_summaries(false, cx)
-                .map(|(path, _, _)| path)
-                .collect::<BTreeSet<_>>();
-            paths.extend(self.path_states.iter().map(|state| state.path.clone()));
-            for path in paths {
-                self.update_paths_tx.unbounded_send((path, None)).unwrap();
-            }
-        });
-    }
-
-    /// Enqueue an update of the excerpts for any path whose diagnostics are known
-    /// to have changed. If a language server id is passed, then only the excerpts for
-    /// that language server's diagnostics will be updated. Otherwise, all stale excerpts
-    /// will be refreshed.
-    pub fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {
-        for (path, server_id) in &self.paths_to_update {
-            if language_server_id.map_or(true, |id| id == *server_id) {
-                self.update_paths_tx
-                    .unbounded_send((path.clone(), Some(*server_id)))
-                    .unwrap();
-            }
-        }
-    }
-
-    fn update_excerpts(
-        &mut self,
-        path_to_update: ProjectPath,
-        server_to_update: Option<LanguageServerId>,
-        buffer: Model<Buffer>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.paths_to_update.retain(|(path, server_id)| {
-            *path != path_to_update
-                || server_to_update.map_or(false, |to_update| *server_id != to_update)
-        });
-
-        // TODO change selections as in the old panel, to the next primary diagnostics
-        // TODO make [shift-]f8 to work, jump to the next block group
-        let _was_empty = self.path_states.is_empty();
-        let path_ix = match self.path_states.binary_search_by(|probe| {
-            project::compare_paths((&probe.path.path, true), (&path_to_update.path, true))
-        }) {
-            Ok(ix) => ix,
-            Err(ix) => {
-                self.path_states.insert(
-                    ix,
-                    PathState {
-                        path: path_to_update.clone(),
-                        diagnostics: Vec::new(),
-                        last_excerpt_id: None,
-                        first_excerpt_id: None,
-                    },
-                );
-                ix
-            }
-        };
-
-        let max_severity = if self.include_warnings {
-            DiagnosticSeverity::WARNING
-        } else {
-            DiagnosticSeverity::ERROR
-        };
-
-        let excerpt_borders = self.excerpt_borders_for_path(path_ix);
-        let path_state = &mut self.path_states[path_ix];
-        let buffer_snapshot = buffer.read(cx).snapshot();
-
-        let mut path_update = PathUpdate::new(
-            excerpt_borders,
-            &buffer_snapshot,
-            server_to_update,
-            max_severity,
-            path_state,
-        );
-        path_update.prepare_excerpt_data(
-            self.context,
-            self.excerpts.read(cx).snapshot(cx),
-            buffer.read(cx).snapshot(),
-            path_state.diagnostics.iter(),
-        );
-        self.excerpts.update(cx, |multi_buffer, cx| {
-            path_update.apply_excerpt_changes(
-                path_state,
-                self.context,
-                buffer_snapshot,
-                multi_buffer,
-                buffer,
-                cx,
-            );
-        });
-
-        let new_multi_buffer_snapshot = self.excerpts.read(cx).snapshot(cx);
-        let blocks_to_insert =
-            path_update.prepare_blocks_to_insert(self.editor.clone(), new_multi_buffer_snapshot);
-
-        let new_block_ids = self.editor.update(cx, |editor, cx| {
-            editor.remove_blocks(std::mem::take(&mut path_update.blocks_to_remove), None, cx);
-            editor.insert_blocks(blocks_to_insert, Some(Autoscroll::fit()), cx)
-        });
-        path_state.diagnostics = path_update.new_blocks(new_block_ids);
-
-        if self.path_states.is_empty() {
-            if self.editor.focus_handle(cx).is_focused(cx) {
-                cx.focus(&self.focus_handle);
-            }
-        } else if self.focus_handle.is_focused(cx) {
-            let focus_handle = self.editor.focus_handle(cx);
-            cx.focus(&focus_handle);
-        }
-
-        #[cfg(test)]
-        self.check_invariants(cx);
-
-        cx.notify();
-    }
-
-    fn excerpt_borders_for_path(&self, path_ix: usize) -> (Option<ExcerptId>, Option<ExcerptId>) {
-        let previous_path_state_ix =
-            Some(path_ix.saturating_sub(1)).filter(|&previous_path_ix| previous_path_ix != path_ix);
-        let next_path_state_ix = path_ix + 1;
-        let start = previous_path_state_ix.and_then(|i| {
-            self.path_states[..=i]
-                .iter()
-                .rev()
-                .find_map(|state| state.last_excerpt_id)
-        });
-        let end = self.path_states[next_path_state_ix..]
-            .iter()
-            .find_map(|state| state.first_excerpt_id);
-        (start, end)
-    }
-
-    #[cfg(test)]
-    fn check_invariants(&self, cx: &mut ViewContext<Self>) {
-        let mut excerpts = Vec::new();
-        for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
-            if let Some(file) = buffer.file() {
-                excerpts.push((id, file.path().clone()));
-            }
-        }
-
-        let mut prev_path = None;
-        for (_, path) in &excerpts {
-            if let Some(prev_path) = prev_path {
-                if path < prev_path {
-                    panic!("excerpts are not sorted by path {:?}", excerpts);
-                }
-            }
-            prev_path = Some(path);
-        }
-    }
-}
-
-impl FocusableView for GroupedDiagnosticsEditor {
-    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Item for GroupedDiagnosticsEditor {
-    type Event = EditorEvent;
-
-    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
-        Editor::to_item_events(event, f)
-    }
-
-    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
-    }
-
-    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
-        self.editor
-            .update(cx, |editor, cx| editor.navigate(data, cx))
-    }
-
-    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
-        Some("Project Diagnostics".into())
-    }
-
-    fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
-        if self.summary.error_count == 0 && self.summary.warning_count == 0 {
-            Label::new("No problems")
-                .color(params.text_color())
-                .into_any_element()
-        } else {
-            h_flex()
-                .gap_1()
-                .when(self.summary.error_count > 0, |then| {
-                    then.child(
-                        h_flex()
-                            .gap_1()
-                            .child(Icon::new(IconName::XCircle).color(Color::Error))
-                            .child(
-                                Label::new(self.summary.error_count.to_string())
-                                    .color(params.text_color()),
-                            ),
-                    )
-                })
-                .when(self.summary.warning_count > 0, |then| {
-                    then.child(
-                        h_flex()
-                            .gap_1()
-                            .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
-                            .child(
-                                Label::new(self.summary.warning_count.to_string())
-                                    .color(params.text_color()),
-                            ),
-                    )
-                })
-                .into_any_element()
-        }
-    }
-
-    fn telemetry_event_text(&self) -> Option<&'static str> {
-        Some("project diagnostics")
-    }
-
-    fn for_each_project_item(
-        &self,
-        cx: &AppContext,
-        f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
-    ) {
-        self.editor.for_each_project_item(cx, f)
-    }
-
-    fn is_singleton(&self, _: &AppContext) -> bool {
-        false
-    }
-
-    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
-        self.editor.update(cx, |editor, _| {
-            editor.set_nav_history(Some(nav_history));
-        });
-    }
-
-    fn clone_on_split(
-        &self,
-        _workspace_id: Option<workspace::WorkspaceId>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<View<Self>>
-    where
-        Self: Sized,
-    {
-        Some(cx.new_view(|cx| {
-            GroupedDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
-        }))
-    }
-
-    fn is_dirty(&self, cx: &AppContext) -> bool {
-        self.excerpts.read(cx).is_dirty(cx)
-    }
-
-    fn has_conflict(&self, cx: &AppContext) -> bool {
-        self.excerpts.read(cx).has_conflict(cx)
-    }
-
-    fn can_save(&self, _: &AppContext) -> bool {
-        true
-    }
-
-    fn save(
-        &mut self,
-        format: bool,
-        project: Model<Project>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        self.editor.save(format, project, cx)
-    }
-
-    fn save_as(
-        &mut self,
-        _: Model<Project>,
-        _: ProjectPath,
-        _: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        unreachable!()
-    }
-
-    fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
-        self.editor.reload(project, cx)
-    }
-
-    fn act_as_type<'a>(
-        &'a self,
-        type_id: TypeId,
-        self_handle: &'a View<Self>,
-        _: &'a AppContext,
-    ) -> Option<AnyView> {
-        if type_id == TypeId::of::<Self>() {
-            Some(self_handle.to_any())
-        } else if type_id == TypeId::of::<Editor>() {
-            Some(self.editor.to_any())
-        } else {
-            None
-        }
-    }
-
-    fn breadcrumb_location(&self) -> ToolbarItemLocation {
-        ToolbarItemLocation::PrimaryLeft
-    }
-
-    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
-        self.editor.breadcrumbs(theme, cx)
-    }
-
-    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
-    }
-}
-
-fn compare_data_locations(
-    old: &DiagnosticData,
-    new: &DiagnosticData,
-    snapshot: &BufferSnapshot,
-) -> Ordering {
-    compare_diagnostics(&old.entry, &new.entry, snapshot)
-        .then_with(|| old.language_server_id.cmp(&new.language_server_id))
-}
-
-fn compare_diagnostics(
-    old: &DiagnosticEntry<language::Anchor>,
-    new: &DiagnosticEntry<language::Anchor>,
-    snapshot: &BufferSnapshot,
-) -> Ordering {
-    compare_diagnostic_ranges(&old.range, &new.range, snapshot)
-        .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
-}
-
-fn compare_diagnostic_ranges(
-    old: &Range<language::Anchor>,
-    new: &Range<language::Anchor>,
-    snapshot: &BufferSnapshot,
-) -> Ordering {
-    // The diagnostics may point to a previously open Buffer for this file.
-    if !old.start.is_valid(snapshot) || !new.start.is_valid(snapshot) {
-        return Ordering::Greater;
-    }
-
-    old.start
-        .to_offset(snapshot)
-        .cmp(&new.start.to_offset(snapshot))
-        .then_with(|| {
-            old.end
-                .to_offset(snapshot)
-                .cmp(&new.end.to_offset(snapshot))
-        })
-}
-
-// TODO wrong? What to do here instead?
-fn compare_diagnostic_range_edges(
-    old: &Range<language::Anchor>,
-    new: &Range<language::Anchor>,
-    snapshot: &BufferSnapshot,
-) -> (Ordering, Ordering) {
-    // The diagnostics may point to a previously open Buffer for this file.
-    let start_cmp = match (old.start.is_valid(snapshot), new.start.is_valid(snapshot)) {
-        (false, false) => old.start.offset.cmp(&new.start.offset),
-        (false, true) => Ordering::Greater,
-        (true, false) => Ordering::Less,
-        (true, true) => old.start.cmp(&new.start, snapshot),
-    };
-
-    let end_cmp = old
-        .end
-        .to_offset(snapshot)
-        .cmp(&new.end.to_offset(snapshot));
-    (start_cmp, end_cmp)
-}
-
-#[derive(Debug)]
-struct PathUpdate {
-    path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
-    latest_excerpt_id: ExcerptId,
-    new_diagnostics: Vec<(DiagnosticData, Option<CustomBlockId>)>,
-    diagnostics_by_row_label: BTreeMap<MultiBufferRow, (editor::Anchor, Vec<usize>)>,
-    blocks_to_remove: HashSet<CustomBlockId>,
-    unchanged_blocks: HashMap<usize, CustomBlockId>,
-    excerpts_with_new_diagnostics: HashSet<ExcerptId>,
-    excerpts_to_remove: Vec<ExcerptId>,
-    excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec<ExcerptId>>,
-    excerpts_to_add: HashMap<ExcerptId, Vec<Range<language::Anchor>>>,
-    first_excerpt_id: Option<ExcerptId>,
-    last_excerpt_id: Option<ExcerptId>,
-}
-
-impl PathUpdate {
-    fn new(
-        path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
-        buffer_snapshot: &BufferSnapshot,
-        server_to_update: Option<LanguageServerId>,
-        max_severity: DiagnosticSeverity,
-        path_state: &PathState,
-    ) -> Self {
-        let mut blocks_to_remove = HashSet::default();
-        let mut removed_groups = HashSet::default();
-        let mut new_diagnostics = path_state
-            .diagnostics
-            .iter()
-            .filter(|(diagnostic_data, _)| {
-                server_to_update.map_or(true, |server_id| {
-                    diagnostic_data.language_server_id != server_id
-                })
-            })
-            .filter(|(diagnostic_data, block_id)| {
-                let diagnostic = &diagnostic_data.entry.diagnostic;
-                let retain = !diagnostic.is_primary || diagnostic.severity <= max_severity;
-                if !retain {
-                    removed_groups.insert(diagnostic.group_id);
-                    blocks_to_remove.insert(*block_id);
-                }
-                retain
-            })
-            .map(|(diagnostic, block_id)| (diagnostic.clone(), Some(*block_id)))
-            .collect::<Vec<_>>();
-        new_diagnostics.retain(|(diagnostic_data, block_id)| {
-            let retain = !removed_groups.contains(&diagnostic_data.entry.diagnostic.group_id);
-            if !retain {
-                if let Some(block_id) = block_id {
-                    blocks_to_remove.insert(*block_id);
-                }
-            }
-            retain
-        });
-        for (server_id, group) in buffer_snapshot
-            .diagnostic_groups(server_to_update)
-            .into_iter()
-            .filter(|(_, group)| {
-                group.entries[group.primary_ix].diagnostic.severity <= max_severity
-            })
-        {
-            for (diagnostic_index, diagnostic) in group.entries.iter().enumerate() {
-                let new_data = DiagnosticData {
-                    language_server_id: server_id,
-                    is_primary: diagnostic_index == group.primary_ix,
-                    entry: diagnostic.clone(),
-                };
-                let (Ok(i) | Err(i)) = new_diagnostics.binary_search_by(|probe| {
-                    compare_data_locations(&probe.0, &new_data, &buffer_snapshot)
-                });
-                new_diagnostics.insert(i, (new_data, None));
-            }
-        }
-
-        let latest_excerpt_id = path_excerpts_borders.0.unwrap_or_else(|| ExcerptId::min());
-        Self {
-            latest_excerpt_id,
-            path_excerpts_borders,
-            new_diagnostics,
-            blocks_to_remove,
-            diagnostics_by_row_label: BTreeMap::new(),
-            excerpts_to_remove: Vec::new(),
-            excerpts_with_new_diagnostics: HashSet::default(),
-            unchanged_blocks: HashMap::default(),
-            excerpts_to_add: HashMap::default(),
-            excerpt_expands: HashMap::default(),
-            first_excerpt_id: None,
-            last_excerpt_id: None,
-        }
-    }
-
-    fn prepare_excerpt_data<'a>(
-        &'a mut self,
-        context: u32,
-        multi_buffer_snapshot: MultiBufferSnapshot,
-        buffer_snapshot: BufferSnapshot,
-        current_diagnostics: impl Iterator<Item = &'a (DiagnosticData, CustomBlockId)> + 'a,
-    ) {
-        let mut current_diagnostics = current_diagnostics.fuse().peekable();
-        let mut excerpts_to_expand =
-            HashMap::<ExcerptId, HashMap<ExpandExcerptDirection, u32>>::default();
-        let mut current_excerpts = path_state_excerpts(
-            self.path_excerpts_borders.0,
-            self.path_excerpts_borders.1,
-            &multi_buffer_snapshot,
-        )
-        .fuse()
-        .peekable();
-
-        for (diagnostic_index, (new_diagnostic, existing_block)) in
-            self.new_diagnostics.iter().enumerate()
-        {
-            if let Some(existing_block) = existing_block {
-                self.unchanged_blocks
-                    .insert(diagnostic_index, *existing_block);
-            }
-
-            loop {
-                match current_excerpts.peek() {
-                    None => {
-                        let excerpt_ranges = self
-                            .excerpts_to_add
-                            .entry(self.latest_excerpt_id)
-                            .or_default();
-                        let new_range = new_diagnostic.entry.range.clone();
-                        let (Ok(i) | Err(i)) = excerpt_ranges.binary_search_by(|probe| {
-                            compare_diagnostic_ranges(probe, &new_range, &buffer_snapshot)
-                        });
-                        excerpt_ranges.insert(i, new_range);
-                        break;
-                    }
-                    Some((current_excerpt_id, _, current_excerpt_range)) => {
-                        match compare_diagnostic_range_edges(
-                            &current_excerpt_range.context,
-                            &new_diagnostic.entry.range,
-                            &buffer_snapshot,
-                        ) {
-                            /*
-                                  new_s new_e
-                            ----[---->><<----]--
-                             cur_s         cur_e
-                            */
-                            (
-                                Ordering::Less | Ordering::Equal,
-                                Ordering::Greater | Ordering::Equal,
-                            ) => {
-                                self.excerpts_with_new_diagnostics
-                                    .insert(*current_excerpt_id);
-                                if self.first_excerpt_id.is_none() {
-                                    self.first_excerpt_id = Some(*current_excerpt_id);
-                                }
-                                self.last_excerpt_id = Some(*current_excerpt_id);
-                                break;
-                            }
-                            /*
-                                  cur_s cur_e
-                            ---->>>>>[--]<<<<<--
-                             new_s         new_e
-                            */
-                            (
-                                Ordering::Greater | Ordering::Equal,
-                                Ordering::Less | Ordering::Equal,
-                            ) => {
-                                let expand_up = current_excerpt_range
-                                    .context
-                                    .start
-                                    .to_point(&buffer_snapshot)
-                                    .row
-                                    .saturating_sub(
-                                        new_diagnostic
-                                            .entry
-                                            .range
-                                            .start
-                                            .to_point(&buffer_snapshot)
-                                            .row,
-                                    );
-                                let expand_down = new_diagnostic
-                                    .entry
-                                    .range
-                                    .end
-                                    .to_point(&buffer_snapshot)
-                                    .row
-                                    .saturating_sub(
-                                        current_excerpt_range
-                                            .context
-                                            .end
-                                            .to_point(&buffer_snapshot)
-                                            .row,
-                                    );
-                                let expand_value = excerpts_to_expand
-                                    .entry(*current_excerpt_id)
-                                    .or_default()
-                                    .entry(ExpandExcerptDirection::UpAndDown)
-                                    .or_default();
-                                *expand_value = (*expand_value).max(expand_up).max(expand_down);
-                                self.excerpts_with_new_diagnostics
-                                    .insert(*current_excerpt_id);
-                                if self.first_excerpt_id.is_none() {
-                                    self.first_excerpt_id = Some(*current_excerpt_id);
-                                }
-                                self.last_excerpt_id = Some(*current_excerpt_id);
-                                break;
-                            }
-                            /*
-                                    new_s   new_e
-                                     >       <
-                            ----[---->>>]<<<<<--
-                             cur_s    cur_e
-
-                            or
-                                      new_s new_e
-                                        >    <
-                            ----[----]-->>><<<--
-                             cur_s cur_e
-                            */
-                            (Ordering::Less, Ordering::Less) => {
-                                if current_excerpt_range
-                                    .context
-                                    .end
-                                    .cmp(&new_diagnostic.entry.range.start, &buffer_snapshot)
-                                    .is_ge()
-                                {
-                                    let expand_down = new_diagnostic
-                                        .entry
-                                        .range
-                                        .end
-                                        .to_point(&buffer_snapshot)
-                                        .row
-                                        .saturating_sub(
-                                            current_excerpt_range
-                                                .context
-                                                .end
-                                                .to_point(&buffer_snapshot)
-                                                .row,
-                                        );
-                                    let expand_value = excerpts_to_expand
-                                        .entry(*current_excerpt_id)
-                                        .or_default()
-                                        .entry(ExpandExcerptDirection::Down)
-                                        .or_default();
-                                    *expand_value = (*expand_value).max(expand_down);
-                                    self.excerpts_with_new_diagnostics
-                                        .insert(*current_excerpt_id);
-                                    if self.first_excerpt_id.is_none() {
-                                        self.first_excerpt_id = Some(*current_excerpt_id);
-                                    }
-                                    self.last_excerpt_id = Some(*current_excerpt_id);
-                                    break;
-                                } else if !self
-                                    .excerpts_with_new_diagnostics
-                                    .contains(current_excerpt_id)
-                                {
-                                    self.excerpts_to_remove.push(*current_excerpt_id);
-                                }
-                            }
-                            /*
-                                  cur_s      cur_e
-                            ---->>>>>[<<<<----]--
-                                >        <
-                               new_s    new_e
-
-                            or
-                                      cur_s cur_e
-                            ---->>><<<--[----]--
-                                >    <
-                               new_s new_e
-                            */
-                            (Ordering::Greater, Ordering::Greater) => {
-                                if current_excerpt_range
-                                    .context
-                                    .start
-                                    .cmp(&new_diagnostic.entry.range.end, &buffer_snapshot)
-                                    .is_le()
-                                {
-                                    let expand_up = current_excerpt_range
-                                        .context
-                                        .start
-                                        .to_point(&buffer_snapshot)
-                                        .row
-                                        .saturating_sub(
-                                            new_diagnostic
-                                                .entry
-                                                .range
-                                                .start
-                                                .to_point(&buffer_snapshot)
-                                                .row,
-                                        );
-                                    let expand_value = excerpts_to_expand
-                                        .entry(*current_excerpt_id)
-                                        .or_default()
-                                        .entry(ExpandExcerptDirection::Up)
-                                        .or_default();
-                                    *expand_value = (*expand_value).max(expand_up);
-                                    self.excerpts_with_new_diagnostics
-                                        .insert(*current_excerpt_id);
-                                    if self.first_excerpt_id.is_none() {
-                                        self.first_excerpt_id = Some(*current_excerpt_id);
-                                    }
-                                    self.last_excerpt_id = Some(*current_excerpt_id);
-                                    break;
-                                } else {
-                                    let excerpt_ranges = self
-                                        .excerpts_to_add
-                                        .entry(self.latest_excerpt_id)
-                                        .or_default();
-                                    let new_range = new_diagnostic.entry.range.clone();
-                                    let (Ok(i) | Err(i)) =
-                                        excerpt_ranges.binary_search_by(|probe| {
-                                            compare_diagnostic_ranges(
-                                                probe,
-                                                &new_range,
-                                                &buffer_snapshot,
-                                            )
-                                        });
-                                    excerpt_ranges.insert(i, new_range);
-                                    break;
-                                }
-                            }
-                        }
-                        if let Some((next_id, ..)) = current_excerpts.next() {
-                            self.latest_excerpt_id = next_id;
-                        }
-                    }
-                }
-            }
-
-            loop {
-                match current_diagnostics.peek() {
-                    None => break,
-                    Some((current_diagnostic, current_block)) => {
-                        match compare_data_locations(
-                            current_diagnostic,
-                            new_diagnostic,
-                            &buffer_snapshot,
-                        ) {
-                            Ordering::Less => {
-                                self.blocks_to_remove.insert(*current_block);
-                            }
-                            Ordering::Equal => {
-                                if current_diagnostic.diagnostic_entries_equal(&new_diagnostic) {
-                                    self.unchanged_blocks
-                                        .insert(diagnostic_index, *current_block);
-                                } else {
-                                    self.blocks_to_remove.insert(*current_block);
-                                }
-                                let _ = current_diagnostics.next();
-                                break;
-                            }
-                            Ordering::Greater => break,
-                        }
-                        let _ = current_diagnostics.next();
-                    }
-                }
-            }
-        }
-
-        self.excerpts_to_remove.retain(|excerpt_id| {
-            !self.excerpts_with_new_diagnostics.contains(excerpt_id)
-                && !excerpts_to_expand.contains_key(excerpt_id)
-        });
-        self.excerpts_to_remove.extend(
-            current_excerpts
-                .filter(|(excerpt_id, ..)| {
-                    !self.excerpts_with_new_diagnostics.contains(excerpt_id)
-                        && !excerpts_to_expand.contains_key(excerpt_id)
-                })
-                .map(|(excerpt_id, ..)| excerpt_id),
-        );
-        let mut excerpt_expands = HashMap::default();
-        for (excerpt_id, directions) in excerpts_to_expand {
-            let excerpt_expand = if directions.len() > 1 {
-                Some((
-                    ExpandExcerptDirection::UpAndDown,
-                    directions
-                        .values()
-                        .max()
-                        .copied()
-                        .unwrap_or_default()
-                        .max(context),
-                ))
-            } else {
-                directions
-                    .into_iter()
-                    .next()
-                    .map(|(direction, expand)| (direction, expand.max(context)))
-            };
-            if let Some(expand) = excerpt_expand {
-                excerpt_expands
-                    .entry(expand)
-                    .or_insert_with(|| Vec::new())
-                    .push(excerpt_id);
-            }
-        }
-        self.blocks_to_remove
-            .extend(current_diagnostics.map(|(_, block_id)| block_id));
-    }
-
-    fn apply_excerpt_changes(
-        &mut self,
-        path_state: &mut PathState,
-        context: u32,
-        buffer_snapshot: BufferSnapshot,
-        multi_buffer: &mut MultiBuffer,
-        buffer: Model<Buffer>,
-        cx: &mut gpui::ModelContext<MultiBuffer>,
-    ) {
-        let max_point = buffer_snapshot.max_point();
-        for (after_excerpt_id, ranges) in std::mem::take(&mut self.excerpts_to_add) {
-            let ranges = ranges
-                .into_iter()
-                .map(|range| {
-                    let mut extended_point_range = range.to_point(&buffer_snapshot);
-                    extended_point_range.start.row =
-                        extended_point_range.start.row.saturating_sub(context);
-                    extended_point_range.start.column = 0;
-                    extended_point_range.end.row =
-                        (extended_point_range.end.row + context).min(max_point.row);
-                    extended_point_range.end.column = u32::MAX;
-                    let extended_start =
-                        buffer_snapshot.clip_point(extended_point_range.start, Bias::Left);
-                    let extended_end =
-                        buffer_snapshot.clip_point(extended_point_range.end, Bias::Right);
-                    extended_start..extended_end
-                })
-                .collect::<Vec<_>>();
-            let (joined_ranges, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context);
-            let excerpts = multi_buffer.insert_excerpts_after(
-                after_excerpt_id,
-                buffer.clone(),
-                joined_ranges,
-                cx,
-            );
-            if self.first_excerpt_id.is_none() {
-                self.first_excerpt_id = excerpts.first().copied();
-            }
-            self.last_excerpt_id = excerpts.last().copied();
-        }
-        for ((direction, line_count), excerpts) in std::mem::take(&mut self.excerpt_expands) {
-            multi_buffer.expand_excerpts(excerpts, line_count, direction, cx);
-        }
-        multi_buffer.remove_excerpts(std::mem::take(&mut self.excerpts_to_remove), cx);
-        path_state.first_excerpt_id = self.first_excerpt_id;
-        path_state.last_excerpt_id = self.last_excerpt_id;
-    }
-
-    fn prepare_blocks_to_insert(
-        &mut self,
-        editor: View<Editor>,
-        multi_buffer_snapshot: MultiBufferSnapshot,
-    ) -> Vec<BlockProperties<editor::Anchor>> {
-        let mut updated_excerpts = path_state_excerpts(
-            self.path_excerpts_borders.0,
-            self.path_excerpts_borders.1,
-            &multi_buffer_snapshot,
-        )
-        .fuse()
-        .peekable();
-        let mut used_labels = BTreeMap::new();
-        self.diagnostics_by_row_label = self.new_diagnostics.iter().enumerate().fold(
-            BTreeMap::new(),
-            |mut diagnostics_by_row_label, (diagnostic_index, (diagnostic, existing_block))| {
-                let new_diagnostic = &diagnostic.entry;
-                let block_position = new_diagnostic.range.start;
-                let excerpt_id = loop {
-                    match updated_excerpts.peek() {
-                        None => break None,
-                        Some((excerpt_id, excerpt_buffer_snapshot, excerpt_range)) => {
-                            let excerpt_range = &excerpt_range.context;
-                            match block_position.cmp(&excerpt_range.start, excerpt_buffer_snapshot)
-                            {
-                                Ordering::Less => break None,
-                                Ordering::Equal | Ordering::Greater => match block_position
-                                    .cmp(&excerpt_range.end, excerpt_buffer_snapshot)
-                                {
-                                    Ordering::Equal | Ordering::Less => break Some(*excerpt_id),
-                                    Ordering::Greater => {
-                                        let _ = updated_excerpts.next();
-                                    }
-                                },
-                            }
-                        }
-                    }
-                };
-
-                let Some(position_in_multi_buffer) = excerpt_id.and_then(|excerpt_id| {
-                    multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, block_position)
-                }) else {
-                    return diagnostics_by_row_label;
-                };
-
-                let multi_buffer_row = MultiBufferRow(
-                    position_in_multi_buffer
-                        .to_point(&multi_buffer_snapshot)
-                        .row,
-                );
-
-                let grouped_diagnostics = &mut diagnostics_by_row_label
-                    .entry(multi_buffer_row)
-                    .or_insert_with(|| (position_in_multi_buffer, Vec::new()))
-                    .1;
-                let new_label = used_labels
-                    .entry(multi_buffer_row)
-                    .or_insert_with(|| HashSet::default())
-                    .insert((
-                        new_diagnostic.diagnostic.source.as_deref(),
-                        new_diagnostic.diagnostic.message.as_str(),
-                    ));
-
-                if !new_label || !grouped_diagnostics.is_empty() {
-                    if let Some(existing_block) = existing_block {
-                        self.blocks_to_remove.insert(*existing_block);
-                    }
-                    if let Some(block_id) = self.unchanged_blocks.remove(&diagnostic_index) {
-                        self.blocks_to_remove.insert(block_id);
-                    }
-                }
-                if new_label {
-                    let (Ok(i) | Err(i)) = grouped_diagnostics.binary_search_by(|&probe| {
-                        let a = &self.new_diagnostics[probe].0.entry.diagnostic;
-                        let b = &self.new_diagnostics[diagnostic_index].0.entry.diagnostic;
-                        a.group_id
-                            .cmp(&b.group_id)
-                            .then_with(|| a.is_primary.cmp(&b.is_primary).reverse())
-                            .then_with(|| a.severity.cmp(&b.severity))
-                    });
-                    grouped_diagnostics.insert(i, diagnostic_index);
-                }
-
-                diagnostics_by_row_label
-            },
-        );
-
-        self.diagnostics_by_row_label
-            .values()
-            .filter_map(|(earliest_in_row_position, diagnostics_at_line)| {
-                let earliest_in_row_position = *earliest_in_row_position;
-                match diagnostics_at_line.len() {
-                    0 => None,
-                    len => {
-                        if len == 1 {
-                            let i = diagnostics_at_line.first().copied()?;
-                            if self.unchanged_blocks.contains_key(&i) {
-                                return None;
-                            }
-                        }
-                        let lines_in_first_message = diagnostic_text_lines(
-                            &self
-                                .new_diagnostics
-                                .get(diagnostics_at_line.first().copied()?)?
-                                .0
-                                .entry
-                                .diagnostic,
-                        );
-                        let folded_block_height = lines_in_first_message.clamp(1, 2);
-                        let diagnostics_to_render = Arc::new(
-                            diagnostics_at_line
-                                .iter()
-                                .filter_map(|&index| self.new_diagnostics.get(index))
-                                .map(|(diagnostic_data, _)| {
-                                    diagnostic_data.entry.diagnostic.clone()
-                                })
-                                .collect::<Vec<_>>(),
-                        );
-                        Some(BlockProperties {
-                            position: earliest_in_row_position,
-                            height: folded_block_height,
-                            style: BlockStyle::Sticky,
-                            render: render_same_line_diagnostics(
-                                Arc::new(AtomicBool::new(false)),
-                                diagnostics_to_render,
-                                editor.clone(),
-                                folded_block_height,
-                            ),
-                            disposition: BlockDisposition::Above,
-                        })
-                    }
-                }
-            })
-            .collect()
-    }
-
-    fn new_blocks(
-        mut self,
-        new_block_ids: Vec<CustomBlockId>,
-    ) -> Vec<(DiagnosticData, CustomBlockId)> {
-        let mut new_block_ids = new_block_ids.into_iter().fuse();
-        for (_, (_, grouped_diagnostics)) in self.diagnostics_by_row_label {
-            let mut created_block_id = None;
-            match grouped_diagnostics.len() {
-                0 => {
-                    debug_panic!("Unexpected empty diagnostics group");
-                    continue;
-                }
-                1 => {
-                    let index = grouped_diagnostics[0];
-                    if let Some(&block_id) = self.unchanged_blocks.get(&index) {
-                        self.new_diagnostics[index].1 = Some(block_id);
-                    } else {
-                        let Some(block_id) =
-                            created_block_id.get_or_insert_with(|| new_block_ids.next())
-                        else {
-                            debug_panic!("Expected a new block for each new diagnostic");
-                            continue;
-                        };
-                        self.new_diagnostics[index].1 = Some(*block_id);
-                    }
-                }
-                _ => {
-                    let Some(block_id) =
-                        created_block_id.get_or_insert_with(|| new_block_ids.next())
-                    else {
-                        debug_panic!("Expected a new block for each new diagnostic group");
-                        continue;
-                    };
-                    for i in grouped_diagnostics {
-                        self.new_diagnostics[i].1 = Some(*block_id);
-                    }
-                }
-            }
-        }
-
-        self.new_diagnostics
-            .into_iter()
-            .filter_map(|(diagnostic, block_id)| Some((diagnostic, block_id?)))
-            .collect()
-    }
-}
-
-fn render_same_line_diagnostics(
-    expanded: Arc<AtomicBool>,
-    diagnostics: Arc<Vec<language::Diagnostic>>,
-    editor_handle: View<Editor>,
-    folded_block_height: u8,
-) -> RenderBlock {
-    Box::new(move |cx: &mut BlockContext| {
-        let block_id = match cx.block_id {
-            BlockId::Custom(block_id) => block_id,
-            _ => {
-                debug_panic!("Expected a block id for the diagnostics block");
-                return div().into_any_element();
-            }
-        };
-        let Some(first_diagnostic) = diagnostics.first() else {
-            debug_panic!("Expected at least one diagnostic");
-            return div().into_any_element();
-        };
-        let button_expanded = expanded.clone();
-        let expanded = expanded.load(atomic::Ordering::Acquire);
-        let expand_label = if expanded { '-' } else { '+' };
-        let first_diagnostics_height = diagnostic_text_lines(first_diagnostic);
-        let extra_diagnostics = diagnostics.len() - 1;
-        let toggle_expand_label =
-            if folded_block_height == first_diagnostics_height && extra_diagnostics == 0 {
-                None
-            } else if extra_diagnostics > 0 {
-                Some(format!("{expand_label}{extra_diagnostics}"))
-            } else {
-                Some(expand_label.to_string())
-            };
-
-        let expanded_block_height = diagnostics
-            .iter()
-            .map(|diagnostic| diagnostic_text_lines(diagnostic))
-            .sum::<u8>();
-        let editor_handle = editor_handle.clone();
-        let parent = h_flex()
-            .items_start()
-            .child(v_flex().size_full().map(|parent| {
-                if let Some(label) = toggle_expand_label {
-                    parent.child(Button::new(cx.block_id, label).on_click({
-                        let diagnostics = Arc::clone(&diagnostics);
-                        move |_, cx| {
-                            let new_expanded = !expanded;
-                            button_expanded.store(new_expanded, atomic::Ordering::Release);
-                            let new_size = if new_expanded {
-                                expanded_block_height
-                            } else {
-                                folded_block_height
-                            };
-                            editor_handle.update(cx, |editor, cx| {
-                                editor.replace_blocks(
-                                    HashMap::from_iter(Some((
-                                        block_id,
-                                        (
-                                            Some(new_size),
-                                            render_same_line_diagnostics(
-                                                Arc::clone(&button_expanded),
-                                                Arc::clone(&diagnostics),
-                                                editor_handle.clone(),
-                                                folded_block_height,
-                                            ),
-                                        ),
-                                    ))),
-                                    None,
-                                    cx,
-                                )
-                            });
-                        }
-                    }))
-                } else {
-                    parent.child(
-                        h_flex()
-                            .size(IconSize::default().rems())
-                            .invisible()
-                            .flex_none(),
-                    )
-                }
-            }));
-        let max_message_rows = if expanded {
-            None
-        } else {
-            Some(folded_block_height)
-        };
-        let mut renderer =
-            diagnostic_block_renderer(first_diagnostic.clone(), max_message_rows, false, true);
-        let mut diagnostics_element = v_flex();
-        diagnostics_element = diagnostics_element.child(renderer(cx));
-        if expanded {
-            for diagnostic in diagnostics.iter().skip(1) {
-                let mut renderer = diagnostic_block_renderer(diagnostic.clone(), None, false, true);
-                diagnostics_element = diagnostics_element.child(renderer(cx));
-            }
-        }
-        parent.child(diagnostics_element).into_any_element()
-    })
-}
-
-fn diagnostic_text_lines(diagnostic: &language::Diagnostic) -> u8 {
-    diagnostic.message.matches('\n').count() as u8 + 1
-}
-
-fn path_state_excerpts(
-    after_excerpt_id: Option<ExcerptId>,
-    before_excerpt_id: Option<ExcerptId>,
-    multi_buffer_snapshot: &editor::MultiBufferSnapshot,
-) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<language::Anchor>)> {
-    multi_buffer_snapshot
-        .excerpts()
-        .skip_while(move |&(excerpt_id, ..)| match after_excerpt_id {
-            Some(after_excerpt_id) => after_excerpt_id != excerpt_id,
-            None => false,
-        })
-        .filter(move |&(excerpt_id, ..)| after_excerpt_id != Some(excerpt_id))
-        .take_while(move |&(excerpt_id, ..)| match before_excerpt_id {
-            Some(before_excerpt_id) => before_excerpt_id != excerpt_id,
-            None => true,
-        })
-}

crates/diagnostics/src/toolbar_controls.rs 🔗

@@ -1,12 +1,11 @@
-use crate::{grouped_diagnostics::GroupedDiagnosticsEditor, ProjectDiagnosticsEditor};
-use futures::future::Either;
+use crate::ProjectDiagnosticsEditor;
 use gpui::{EventEmitter, ParentElement, Render, View, ViewContext, WeakView};
 use ui::prelude::*;
 use ui::{IconButton, IconName, Tooltip};
 use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 
 pub struct ToolbarControls {
-    editor: Option<Either<WeakView<ProjectDiagnosticsEditor>, WeakView<GroupedDiagnosticsEditor>>>,
+    editor: Option<WeakView<ProjectDiagnosticsEditor>>,
 }
 
 impl Render for ToolbarControls {
@@ -16,32 +15,16 @@ impl Render for ToolbarControls {
         let mut is_updating = false;
 
         if let Some(editor) = self.editor() {
-            match editor {
-                Either::Left(editor) => {
-                    let editor = editor.read(cx);
-                    include_warnings = editor.include_warnings;
-                    has_stale_excerpts = !editor.paths_to_update.is_empty();
-                    is_updating = editor.update_paths_tx.len() > 0
-                        || editor
-                            .project
-                            .read(cx)
-                            .language_servers_running_disk_based_diagnostics()
-                            .next()
-                            .is_some();
-                }
-                Either::Right(editor) => {
-                    let editor = editor.read(cx);
-                    include_warnings = editor.include_warnings;
-                    has_stale_excerpts = !editor.paths_to_update.is_empty();
-                    is_updating = editor.update_paths_tx.len() > 0
-                        || editor
-                            .project
-                            .read(cx)
-                            .language_servers_running_disk_based_diagnostics()
-                            .next()
-                            .is_some();
-                }
-            }
+            let editor = editor.read(cx);
+            include_warnings = editor.include_warnings;
+            has_stale_excerpts = !editor.paths_to_update.is_empty();
+            is_updating = editor.update_paths_tx.len() > 0
+                || editor
+                    .project
+                    .read(cx)
+                    .language_servers_running_disk_based_diagnostics()
+                    .next()
+                    .is_some();
         }
 
         let tooltip = if include_warnings {
@@ -59,18 +42,9 @@ impl Render for ToolbarControls {
                         .tooltip(move |cx| Tooltip::text("Update excerpts", cx))
                         .on_click(cx.listener(|this, _, cx| {
                             if let Some(editor) = this.editor() {
-                                match editor {
-                                    Either::Left(editor) => {
-                                        editor.update(cx, |editor, _| {
-                                            editor.enqueue_update_stale_excerpts(None);
-                                        });
-                                    }
-                                    Either::Right(editor) => {
-                                        editor.update(cx, |editor, _| {
-                                            editor.enqueue_update_stale_excerpts(None);
-                                        });
-                                    }
-                                }
+                                editor.update(cx, |editor, _| {
+                                    editor.enqueue_update_stale_excerpts(None);
+                                });
                             }
                         })),
                 )
@@ -80,18 +54,9 @@ impl Render for ToolbarControls {
                     .tooltip(move |cx| Tooltip::text(tooltip, cx))
                     .on_click(cx.listener(|this, _, cx| {
                         if let Some(editor) = this.editor() {
-                            match editor {
-                                Either::Left(editor) => {
-                                    editor.update(cx, |editor, cx| {
-                                        editor.toggle_warnings(&Default::default(), cx);
-                                    });
-                                }
-                                Either::Right(editor) => {
-                                    editor.update(cx, |editor, cx| {
-                                        editor.toggle_warnings(&Default::default(), cx);
-                                    });
-                                }
-                            }
+                            editor.update(cx, |editor, cx| {
+                                editor.toggle_warnings(&Default::default(), cx);
+                            });
                         }
                     })),
             )
@@ -108,10 +73,7 @@ impl ToolbarItemView for ToolbarControls {
     ) -> ToolbarItemLocation {
         if let Some(pane_item) = active_pane_item.as_ref() {
             if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
-                self.editor = Some(Either::Left(editor.downgrade()));
-                ToolbarItemLocation::PrimaryRight
-            } else if let Some(editor) = pane_item.downcast::<GroupedDiagnosticsEditor>() {
-                self.editor = Some(Either::Right(editor.downgrade()));
+                self.editor = Some(editor.downgrade());
                 ToolbarItemLocation::PrimaryRight
             } else {
                 ToolbarItemLocation::Hidden
@@ -127,12 +89,7 @@ impl ToolbarControls {
         ToolbarControls { editor: None }
     }
 
-    fn editor(
-        &self,
-    ) -> Option<Either<View<ProjectDiagnosticsEditor>, View<GroupedDiagnosticsEditor>>> {
-        Some(match self.editor.as_ref()? {
-            Either::Left(diagnostics) => Either::Left(diagnostics.upgrade()?),
-            Either::Right(grouped_diagnostics) => Either::Right(grouped_diagnostics.upgrade()?),
-        })
+    fn editor(&self) -> Option<View<ProjectDiagnosticsEditor>> {
+        self.editor.as_ref()?.upgrade()
     }
 }

crates/editor/src/display_map.rs 🔗

@@ -120,9 +120,9 @@ impl DisplayMap {
         font_size: Pixels,
         wrap_width: Option<Pixels>,
         show_excerpt_controls: bool,
-        buffer_header_height: u8,
-        excerpt_header_height: u8,
-        excerpt_footer_height: u8,
+        buffer_header_height: u32,
+        excerpt_header_height: u32,
+        excerpt_footer_height: u32,
         fold_placeholder: FoldPlaceholder,
         cx: &mut ModelContext<Self>,
     ) -> Self {
@@ -286,44 +286,11 @@ impl DisplayMap {
         block_map.insert(blocks)
     }
 
-    pub fn replace_blocks(
+    pub fn resize_blocks(
         &mut self,
-        heights_and_renderers: HashMap<CustomBlockId, (Option<u8>, RenderBlock)>,
+        heights: HashMap<CustomBlockId, u32>,
         cx: &mut ModelContext<Self>,
     ) {
-        //
-        // Note: previous implementation of `replace_blocks` simply called
-        // `self.block_map.replace(styles)` which just modified the render by replacing
-        // the `RenderBlock` with the new one.
-        //
-        // ```rust
-        //  for block in &self.blocks {
-        //           if let Some(render) = renderers.remove(&block.id) {
-        //               *block.render.lock() = render;
-        //           }
-        //       }
-        // ```
-        //
-        // If height changes however, we need to update the tree. There's a performance
-        // cost to this, so we'll split the replace blocks into handling the old behavior
-        // directly and the new behavior separately.
-        //
-        //
-        let mut only_renderers = HashMap::<CustomBlockId, RenderBlock>::default();
-        let mut full_replace = HashMap::<CustomBlockId, (u8, RenderBlock)>::default();
-        for (id, (height, render)) in heights_and_renderers {
-            if let Some(height) = height {
-                full_replace.insert(id, (height, render));
-            } else {
-                only_renderers.insert(id, render);
-            }
-        }
-        self.block_map.replace_renderers(only_renderers);
-
-        if full_replace.is_empty() {
-            return;
-        }
-
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
@@ -334,7 +301,11 @@ impl DisplayMap {
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         let mut block_map = self.block_map.write(snapshot, edits);
-        block_map.replace(full_replace);
+        block_map.resize(heights);
+    }
+
+    pub fn replace_blocks(&mut self, renderers: HashMap<CustomBlockId, RenderBlock>) {
+        self.block_map.replace_blocks(renderers);
     }
 
     pub fn remove_blocks(&mut self, ids: HashSet<CustomBlockId>, cx: &mut ModelContext<Self>) {
@@ -1051,6 +1022,18 @@ impl DisplaySnapshot {
         let type_id = TypeId::of::<Tag>();
         self.inlay_highlights.get(&type_id)
     }
+
+    pub fn buffer_header_height(&self) -> u32 {
+        self.block_snapshot.buffer_header_height
+    }
+
+    pub fn excerpt_footer_height(&self) -> u32 {
+        self.block_snapshot.excerpt_footer_height
+    }
+
+    pub fn excerpt_header_height(&self) -> u32 {
+        self.block_snapshot.excerpt_header_height
+    }
 }
 
 #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]

crates/editor/src/display_map/block_map.rs 🔗

@@ -35,9 +35,9 @@ pub struct BlockMap {
     custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
     transforms: RefCell<SumTree<Transform>>,
     show_excerpt_controls: bool,
-    buffer_header_height: u8,
-    excerpt_header_height: u8,
-    excerpt_footer_height: u8,
+    buffer_header_height: u32,
+    excerpt_header_height: u32,
+    excerpt_footer_height: u32,
 }
 
 pub struct BlockMapReader<'a> {
@@ -52,6 +52,9 @@ pub struct BlockSnapshot {
     wrap_snapshot: WrapSnapshot,
     transforms: SumTree<Transform>,
     custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
+    pub(super) buffer_header_height: u32,
+    pub(super) excerpt_header_height: u32,
+    pub(super) excerpt_footer_height: u32,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -77,15 +80,15 @@ pub type RenderBlock = Box<dyn Send + FnMut(&mut BlockContext) -> AnyElement>;
 pub struct CustomBlock {
     id: CustomBlockId,
     position: Anchor,
-    height: u8,
+    height: u32,
     style: BlockStyle,
-    render: Mutex<RenderBlock>,
+    render: Arc<Mutex<RenderBlock>>,
     disposition: BlockDisposition,
 }
 
 pub struct BlockProperties<P> {
     pub position: P,
-    pub height: u8,
+    pub height: u32,
     pub style: BlockStyle,
     pub render: RenderBlock,
     pub disposition: BlockDisposition,
@@ -189,14 +192,14 @@ pub enum Block {
         id: ExcerptId,
         buffer: BufferSnapshot,
         range: ExcerptRange<text::Anchor>,
-        height: u8,
+        height: u32,
         starts_new_buffer: bool,
         show_excerpt_controls: bool,
     },
     ExcerptFooter {
         id: ExcerptId,
         disposition: BlockDisposition,
-        height: u8,
+        height: u32,
     },
 }
 
@@ -231,7 +234,7 @@ impl Block {
         }
     }
 
-    pub fn height(&self) -> u8 {
+    pub fn height(&self) -> u32 {
         match self {
             Block::Custom(block) => block.height,
             Block::ExcerptHeader { height, .. } => *height,
@@ -301,9 +304,9 @@ impl BlockMap {
     pub fn new(
         wrap_snapshot: WrapSnapshot,
         show_excerpt_controls: bool,
-        buffer_header_height: u8,
-        excerpt_header_height: u8,
-        excerpt_footer_height: u8,
+        buffer_header_height: u32,
+        excerpt_header_height: u32,
+        excerpt_footer_height: u32,
     ) -> Self {
         let row_count = wrap_snapshot.max_point().row() + 1;
         let map = Self {
@@ -336,6 +339,9 @@ impl BlockMap {
                 wrap_snapshot,
                 transforms: self.transforms.borrow().clone(),
                 custom_blocks_by_id: self.custom_blocks_by_id.clone(),
+                buffer_header_height: self.buffer_header_height,
+                excerpt_header_height: self.excerpt_header_height,
+                excerpt_footer_height: self.excerpt_footer_height,
             },
         }
     }
@@ -551,7 +557,7 @@ impl BlockMap {
         *transforms = new_transforms;
     }
 
-    pub fn replace_renderers(&mut self, mut renderers: HashMap<CustomBlockId, RenderBlock>) {
+    pub fn replace_blocks(&mut self, mut renderers: HashMap<CustomBlockId, RenderBlock>) {
         for block in &mut self.custom_blocks {
             if let Some(render) = renderers.remove(&block.id) {
                 *block.render.lock() = render;
@@ -565,9 +571,9 @@ impl BlockMap {
 
     pub fn header_and_footer_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>(
         show_excerpt_controls: bool,
-        excerpt_footer_height: u8,
-        buffer_header_height: u8,
-        excerpt_header_height: u8,
+        excerpt_footer_height: u32,
+        buffer_header_height: u32,
+        excerpt_header_height: u32,
         buffer: &'b multi_buffer::MultiBufferSnapshot,
         range: R,
         wrap_snapshot: &'c WrapSnapshot,
@@ -793,7 +799,7 @@ impl<'a> BlockMapWriter<'a> {
                 id,
                 position,
                 height: block.height,
-                render: Mutex::new(block.render),
+                render: Arc::new(Mutex::new(block.render)),
                 disposition: block.disposition,
                 style: block.style,
             });
@@ -810,24 +816,21 @@ impl<'a> BlockMapWriter<'a> {
         ids
     }
 
-    pub fn replace(
-        &mut self,
-        mut heights_and_renderers: HashMap<CustomBlockId, (u8, RenderBlock)>,
-    ) {
+    pub fn resize(&mut self, mut heights: HashMap<CustomBlockId, u32>) {
         let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
         let buffer = wrap_snapshot.buffer_snapshot();
         let mut edits = Patch::default();
         let mut last_block_buffer_row = None;
 
         for block in &mut self.0.custom_blocks {
-            if let Some((new_height, render)) = heights_and_renderers.remove(&block.id) {
+            if let Some(new_height) = heights.remove(&block.id) {
                 if block.height != new_height {
                     let new_block = CustomBlock {
                         id: block.id,
                         position: block.position,
                         height: new_height,
                         style: block.style,
-                        render: Mutex::new(render),
+                        render: block.render.clone(),
                         disposition: block.disposition,
                     };
                     let new_block = Arc::new(new_block);
@@ -1174,7 +1177,7 @@ impl Transform {
         Self {
             summary: TransformSummary {
                 input_rows: 0,
-                output_rows: block.height() as u32,
+                output_rows: block.height(),
             },
             block: Some(block),
         }
@@ -1445,7 +1448,7 @@ mod tests {
             .blocks_in_range(0..8)
             .map(|(start_row, block)| {
                 let block = block.as_custom().unwrap();
-                (start_row..start_row + block.height as u32, block.id)
+                (start_row..start_row + block.height, block.id)
             })
             .collect::<Vec<_>>();
 
@@ -1697,10 +1700,9 @@ mod tests {
 
             let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
 
-            let mut hash_map = HashMap::default();
-            let render: RenderBlock = Box::new(|_| div().into_any());
-            hash_map.insert(block_ids[0], (2_u8, render));
-            block_map_writer.replace(hash_map);
+            let mut new_heights = HashMap::default();
+            new_heights.insert(block_ids[0], 2);
+            block_map_writer.resize(new_heights);
             let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
             assert_eq!(snapshot.text(), "aaa\n\n\n\n\nbbb\nccc\nddd\n\n\n");
         }
@@ -1708,10 +1710,9 @@ mod tests {
         {
             let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
 
-            let mut hash_map = HashMap::default();
-            let render: RenderBlock = Box::new(|_| div().into_any());
-            hash_map.insert(block_ids[0], (1_u8, render));
-            block_map_writer.replace(hash_map);
+            let mut new_heights = HashMap::default();
+            new_heights.insert(block_ids[0], 1);
+            block_map_writer.resize(new_heights);
 
             let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
             assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
@@ -1720,10 +1721,9 @@ mod tests {
         {
             let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
 
-            let mut hash_map = HashMap::default();
-            let render: RenderBlock = Box::new(|_| div().into_any());
-            hash_map.insert(block_ids[0], (0_u8, render));
-            block_map_writer.replace(hash_map);
+            let mut new_heights = HashMap::default();
+            new_heights.insert(block_ids[0], 0);
+            block_map_writer.resize(new_heights);
 
             let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
             assert_eq!(snapshot.text(), "aaa\n\n\nbbb\nccc\nddd\n\n\n");
@@ -1732,10 +1732,9 @@ mod tests {
         {
             let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
 
-            let mut hash_map = HashMap::default();
-            let render: RenderBlock = Box::new(|_| div().into_any());
-            hash_map.insert(block_ids[0], (3_u8, render));
-            block_map_writer.replace(hash_map);
+            let mut new_heights = HashMap::default();
+            new_heights.insert(block_ids[0], 3);
+            block_map_writer.resize(new_heights);
 
             let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
             assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
@@ -1744,10 +1743,9 @@ mod tests {
         {
             let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
 
-            let mut hash_map = HashMap::default();
-            let render: RenderBlock = Box::new(|_| div().into_any());
-            hash_map.insert(block_ids[0], (3_u8, render));
-            block_map_writer.replace(hash_map);
+            let mut new_heights = HashMap::default();
+            new_heights.insert(block_ids[0], 3);
+            block_map_writer.resize(new_heights);
 
             let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
             // Same height as before, should remain the same
@@ -2185,17 +2183,17 @@ mod tests {
         #[derive(Debug, Eq, PartialEq)]
         enum ExpectedBlock {
             ExcerptHeader {
-                height: u8,
+                height: u32,
                 starts_new_buffer: bool,
             },
             ExcerptFooter {
-                height: u8,
+                height: u32,
                 disposition: BlockDisposition,
             },
             Custom {
                 disposition: BlockDisposition,
                 id: CustomBlockId,
-                height: u8,
+                height: u32,
             },
         }
 
@@ -2214,7 +2212,7 @@ mod tests {
         }
 
         impl ExpectedBlock {
-            fn height(&self) -> u8 {
+            fn height(&self) -> u32 {
                 match self {
                     ExpectedBlock::ExcerptHeader { height, .. } => *height,
                     ExpectedBlock::Custom { height, .. } => *height,

crates/editor/src/editor.rs 🔗

@@ -160,9 +160,9 @@ use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
 use crate::hover_links::find_url;
 use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
 
-pub const FILE_HEADER_HEIGHT: u8 = 1;
-pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u8 = 1;
-pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u8 = 1;
+pub const FILE_HEADER_HEIGHT: u32 = 1;
+pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
+pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u32 = 1;
 pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
@@ -558,7 +558,7 @@ pub struct Editor {
     tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
     tasks_update_task: Option<Task<()>>,
     previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
-    file_header_size: u8,
+    file_header_size: u32,
     breadcrumb_header: Option<String>,
     focused_block: Option<FocusedBlock>,
 }
@@ -9805,14 +9805,11 @@ impl Editor {
                 for (block_id, diagnostic) in &active_diagnostics.blocks {
                     new_styles.insert(
                         *block_id,
-                        (
-                            None,
-                            diagnostic_block_renderer(diagnostic.clone(), None, true, is_valid),
-                        ),
+                        diagnostic_block_renderer(diagnostic.clone(), None, true, is_valid),
                     );
                 }
-                self.display_map.update(cx, |display_map, cx| {
-                    display_map.replace_blocks(new_styles, cx)
+                self.display_map.update(cx, |display_map, _cx| {
+                    display_map.replace_blocks(new_styles)
                 });
             }
         }
@@ -9855,7 +9852,7 @@ impl Editor {
                 .insert_blocks(
                     diagnostic_group.iter().map(|entry| {
                         let diagnostic = entry.diagnostic.clone();
-                        let message_height = diagnostic.message.matches('\n').count() as u8 + 1;
+                        let message_height = diagnostic.message.matches('\n').count() as u32 + 1;
                         BlockProperties {
                             style: BlockStyle::Fixed,
                             position: buffer.anchor_after(entry.range.start),
@@ -10170,16 +10167,31 @@ impl Editor {
         blocks
     }
 
+    pub(crate) fn resize_blocks(
+        &mut self,
+        heights: HashMap<CustomBlockId, u32>,
+        autoscroll: Option<Autoscroll>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.display_map
+            .update(cx, |display_map, cx| display_map.resize_blocks(heights, cx));
+        if let Some(autoscroll) = autoscroll {
+            self.request_autoscroll(autoscroll, cx);
+        }
+    }
+
     pub fn replace_blocks(
         &mut self,
-        blocks: HashMap<CustomBlockId, (Option<u8>, RenderBlock)>,
+        renderers: HashMap<CustomBlockId, RenderBlock>,
         autoscroll: Option<Autoscroll>,
         cx: &mut ViewContext<Self>,
     ) {
         self.display_map
-            .update(cx, |display_map, cx| display_map.replace_blocks(blocks, cx));
+            .update(cx, |display_map, _cx| display_map.replace_blocks(renderers));
         if let Some(autoscroll) = autoscroll {
             self.request_autoscroll(autoscroll, cx);
+        } else {
+            cx.notify();
         }
     }
 
@@ -11755,7 +11767,7 @@ impl Editor {
         })
     }
 
-    pub fn file_header_size(&self) -> u8 {
+    pub fn file_header_size(&self) -> u32 {
         self.file_header_size
     }
 

crates/editor/src/element.rs 🔗

@@ -17,14 +17,14 @@ use crate::{
     hunk_diff::ExpandedHunk,
     hunk_status,
     items::BufferSearchHighlights,
-    mouse_context_menu::MenuPosition,
-    mouse_context_menu::{self, MouseContextMenu},
+    mouse_context_menu::{self, MenuPosition, MouseContextMenu},
     scroll::scroll_amount::ScrollAmount,
-    BlockId, CodeActionsMenu, CursorShape, DisplayPoint, DisplayRow, DocumentHighlightRead,
-    DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
-    ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor,
-    HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RangeToAnchorExt, RowExt,
-    RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
+    BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
+    DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
+    EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
+    HalfPageUp, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
+    Point, RangeToAnchorExt, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
+    CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
 };
 use client::ParticipantIndex;
 use collections::{BTreeMap, HashMap};
@@ -1929,7 +1929,7 @@ impl EditorElement {
     fn render_block(
         &self,
         block: &Block,
-        available_space: Size<AvailableSpace>,
+        available_width: AvailableSpace,
         block_id: BlockId,
         block_row_start: DisplayRow,
         snapshot: &EditorSnapshot,
@@ -1941,6 +1941,7 @@ impl EditorElement {
         em_width: Pixels,
         text_hitbox: &Hitbox,
         scroll_width: &mut Pixels,
+        resized_blocks: &mut HashMap<CustomBlockId, u32>,
         cx: &mut WindowContext,
     ) -> (AnyElement, Size<Pixels>) {
         let mut element = match block {
@@ -2021,7 +2022,7 @@ impl EditorElement {
                     };
 
                     let line_offset_from_top =
-                        block_row_start.0 + *height as u32 + offset_from_excerpt_start
+                        block_row_start.0 + *height + offset_from_excerpt_start
                             - snapshot
                                 .scroll_anchor
                                 .scroll_position(&snapshot.display_snapshot)
@@ -2054,12 +2055,13 @@ impl EditorElement {
 
                     v_flex()
                         .id(("path excerpt header", EntityId::from(block_id)))
-                        .size_full()
-                        .p(header_padding)
+                        .w_full()
+                        .px(header_padding)
                         .child(
                             h_flex()
                                 .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
                                 .id("path header block")
+                                .h(2. * cx.line_height())
                                 .pl(gpui::px(12.))
                                 .pr(gpui::px(8.))
                                 .rounded_md()
@@ -2112,6 +2114,7 @@ impl EditorElement {
                         .children(show_excerpt_controls.then(|| {
                             h_flex()
                                 .flex_basis(Length::Definite(DefiniteLength::Fraction(0.333)))
+                                .h(1. * cx.line_height())
                                 .pt_1()
                                 .justify_end()
                                 .flex_none()
@@ -2157,7 +2160,8 @@ impl EditorElement {
                 } else {
                     v_flex()
                         .id(("excerpt header", EntityId::from(block_id)))
-                        .size_full()
+                        .w_full()
+                        .h(snapshot.excerpt_header_height() as f32 * cx.line_height())
                         .child(
                             div()
                                 .flex()
@@ -2309,7 +2313,8 @@ impl EditorElement {
             Block::ExcerptFooter { id, .. } => {
                 let element = v_flex()
                     .id(("excerpt footer", EntityId::from(block_id)))
-                    .size_full()
+                    .w_full()
+                    .h(snapshot.excerpt_footer_height() as f32 * cx.line_height())
                     .child(
                         h_flex()
                             .justify_end()
@@ -2357,8 +2362,24 @@ impl EditorElement {
             }
         };
 
-        let size = element.layout_as_root(available_space, cx);
-        (element, size)
+        // Discover the element's content height, then round up to the nearest multiple of line height.
+        let preliminary_size =
+            element.layout_as_root(size(available_width, AvailableSpace::MinContent), cx);
+        let quantized_height = (preliminary_size.height / line_height).ceil() * line_height;
+        let final_size = if preliminary_size.height == quantized_height {
+            preliminary_size
+        } else {
+            element.layout_as_root(size(available_width, quantized_height.into()), cx)
+        };
+
+        if let BlockId::Custom(custom_block_id) = block_id {
+            let element_height_in_lines = (final_size.height / line_height).ceil() as u32;
+            if element_height_in_lines != block.height() {
+                resized_blocks.insert(custom_block_id, element_height_in_lines);
+            }
+        }
+
+        (element, final_size)
     }
 
     #[allow(clippy::too_many_arguments)]
@@ -2375,7 +2396,7 @@ impl EditorElement {
         line_height: Pixels,
         line_layouts: &[LineWithInvisibles],
         cx: &mut WindowContext,
-    ) -> Vec<BlockLayout> {
+    ) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
         let (fixed_blocks, non_fixed_blocks) = snapshot
             .blocks_in_range(rows.clone())
             .partition::<Vec<_>, _>(|(_, block)| block.style() == BlockStyle::Fixed);
@@ -2385,11 +2406,9 @@ impl EditorElement {
             .update(cx, |editor, _| editor.take_focused_block());
         let mut fixed_block_max_width = Pixels::ZERO;
         let mut blocks = Vec::new();
+        let mut resized_blocks = HashMap::default();
+
         for (row, block) in fixed_blocks {
-            let available_space = size(
-                AvailableSpace::MinContent,
-                AvailableSpace::Definite(block.height() as f32 * line_height),
-            );
             let block_id = block.id();
 
             if focused_block.as_ref().map_or(false, |b| b.id == block_id) {
@@ -2398,7 +2417,7 @@ impl EditorElement {
 
             let (element, element_size) = self.render_block(
                 block,
-                available_space,
+                AvailableSpace::MinContent,
                 block_id,
                 row,
                 snapshot,
@@ -2410,6 +2429,7 @@ impl EditorElement {
                 em_width,
                 text_hitbox,
                 scroll_width,
+                &mut resized_blocks,
                 cx,
             );
             fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
@@ -2417,7 +2437,7 @@ impl EditorElement {
                 id: block_id,
                 row: Some(row),
                 element,
-                available_space,
+                available_space: size(AvailableSpace::MinContent, element_size.height.into()),
                 style: BlockStyle::Fixed,
             });
         }
@@ -2432,19 +2452,15 @@ impl EditorElement {
                     .max(gutter_dimensions.width + *scroll_width),
                 BlockStyle::Fixed => unreachable!(),
             };
-            let available_space = size(
-                AvailableSpace::Definite(width),
-                AvailableSpace::Definite(block.height() as f32 * line_height),
-            );
             let block_id = block.id();
 
             if focused_block.as_ref().map_or(false, |b| b.id == block_id) {
                 focused_block = None;
             }
 
-            let (element, _) = self.render_block(
+            let (element, element_size) = self.render_block(
                 block,
-                available_space,
+                width.into(),
                 block_id,
                 row,
                 snapshot,
@@ -2456,13 +2472,15 @@ impl EditorElement {
                 em_width,
                 text_hitbox,
                 scroll_width,
+                &mut resized_blocks,
                 cx,
             );
+
             blocks.push(BlockLayout {
                 id: block_id,
                 row: Some(row),
                 element,
-                available_space,
+                available_space: size(width.into(), element_size.height.into()),
                 style,
             });
         }
@@ -2483,14 +2501,10 @@ impl EditorElement {
                             ),
                             BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
                         };
-                        let available_space = size(
-                            width,
-                            AvailableSpace::Definite(block.height() as f32 * line_height),
-                        );
 
-                        let (element, _) = self.render_block(
+                        let (element, element_size) = self.render_block(
                             &block,
-                            available_space,
+                            width,
                             focused_block.id,
                             rows.end,
                             snapshot,
@@ -2502,6 +2516,7 @@ impl EditorElement {
                             em_width,
                             text_hitbox,
                             scroll_width,
+                            &mut resized_blocks,
                             cx,
                         );
 
@@ -2509,7 +2524,7 @@ impl EditorElement {
                             id: block.id(),
                             row: None,
                             element,
-                            available_space,
+                            available_space: size(width, element_size.height.into()),
                             style,
                         });
                     }
@@ -2517,10 +2532,16 @@ impl EditorElement {
             }
         }
 
-        *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width);
-        blocks
+        if resized_blocks.is_empty() {
+            *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width);
+            Ok(blocks)
+        } else {
+            Err(resized_blocks)
+        }
     }
 
+    /// Returns true if any of the blocks changed size since the previous frame. This will trigger
+    /// a restart of rendering for the editor based on the new sizes.
     fn layout_blocks(
         &self,
         blocks: &mut Vec<BlockLayout>,
@@ -4938,21 +4959,27 @@ impl Element for EditorElement {
                         editor.gutter_dimensions = gutter_dimensions;
                         editor.set_visible_line_count(bounds.size.height / line_height, cx);
 
-                        let editor_width =
-                            text_width - gutter_dimensions.margin - overscroll.width - em_width;
-                        let wrap_width = match editor.soft_wrap_mode(cx) {
-                            SoftWrap::None => None,
-                            SoftWrap::PreferLine => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
-                            SoftWrap::EditorWidth => Some(editor_width),
-                            SoftWrap::Column(column) => {
-                                Some(editor_width.min(column as f32 * em_advance))
-                            }
-                        };
-
-                        if editor.set_wrap_width(wrap_width, cx) {
-                            editor.snapshot(cx)
-                        } else {
+                        if matches!(editor.mode, EditorMode::AutoHeight { .. }) {
                             snapshot
+                        } else {
+                            let editor_width =
+                                text_width - gutter_dimensions.margin - overscroll.width - em_width;
+                            let wrap_width = match editor.soft_wrap_mode(cx) {
+                                SoftWrap::None => None,
+                                SoftWrap::PreferLine => {
+                                    Some((MAX_LINE_LEN / 2) as f32 * em_advance)
+                                }
+                                SoftWrap::EditorWidth => Some(editor_width),
+                                SoftWrap::Column(column) => {
+                                    Some(editor_width.min(column as f32 * em_advance))
+                                }
+                            };
+
+                            if editor.set_wrap_width(wrap_width, cx) {
+                                editor.snapshot(cx)
+                            } else {
+                                snapshot
+                            }
                         }
                     });
 
@@ -4995,11 +5022,13 @@ impl Element for EditorElement {
                         }
                     };
 
+                    let mut autoscroll_request = None;
                     let mut autoscroll_containing_element = false;
                     let mut autoscroll_horizontally = false;
                     self.editor.update(cx, |editor, cx| {
+                        autoscroll_request = editor.autoscroll_request();
                         autoscroll_containing_element =
-                            editor.autoscroll_requested() || editor.has_pending_selection();
+                            autoscroll_request.is_some() || editor.has_pending_selection();
                         autoscroll_horizontally =
                             editor.autoscroll_vertically(bounds, line_height, max_scroll_top, cx);
                         snapshot = editor.snapshot(cx);
@@ -5116,7 +5145,7 @@ impl Element for EditorElement {
                     let mut scroll_width =
                         longest_line_width.max(max_visible_line_width) + overscroll.width;
 
-                    let mut blocks = cx.with_element_namespace("blocks", |cx| {
+                    let blocks = cx.with_element_namespace("blocks", |cx| {
                         self.render_blocks(
                             start_row..end_row,
                             &snapshot,
@@ -5131,6 +5160,15 @@ impl Element for EditorElement {
                             cx,
                         )
                     });
+                    let mut blocks = match blocks {
+                        Ok(blocks) => blocks,
+                        Err(resized_blocks) => {
+                            self.editor.update(cx, |editor, cx| {
+                                editor.resize_blocks(resized_blocks, autoscroll_request, cx)
+                            });
+                            return self.prepaint(None, bounds, &mut (), cx);
+                        }
+                    };
 
                     let start_buffer_row =
                         MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
@@ -6430,7 +6468,7 @@ mod tests {
                         disposition: BlockDisposition::Above,
                         height: 3,
                         position: Anchor::min(),
-                        render: Box::new(|_| div().into_any()),
+                        render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()),
                     }],
                     None,
                     cx,

crates/editor/src/hunk_diff.rs 🔗

@@ -364,7 +364,7 @@ impl Editor {
                         .row;
                     let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
                     let line_count = diff_end_row - diff_start_row;
-                    line_count as u8
+                    line_count
                 })?;
                 Some((diff_base_buffer, deleted_text_lines))
             } else {
@@ -422,7 +422,7 @@ impl Editor {
     fn insert_deleted_text_block(
         &mut self,
         diff_base_buffer: Model<Buffer>,
-        deleted_text_height: u8,
+        deleted_text_height: u32,
         hunk: &HoveredHunk,
         cx: &mut ViewContext<'_, Self>,
     ) -> Option<CustomBlockId> {
@@ -431,10 +431,11 @@ impl Editor {
             editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
         let editor = cx.view().clone();
         let hunk = hunk.clone();
+        let height = editor_height.max(deleted_text_height);
         let mut new_block_ids = self.insert_blocks(
             Some(BlockProperties {
                 position: hunk.multi_buffer_range.start,
-                height: editor_height.max(deleted_text_height),
+                height,
                 style: BlockStyle::Flex,
                 disposition: BlockDisposition::Above,
                 render: Box::new(move |cx| {
@@ -474,7 +475,8 @@ impl Editor {
                     h_flex()
                         .id("gutter with editor")
                         .bg(deleted_hunk_color)
-                        .size_full()
+                        .h(height as f32 * cx.line_height())
+                        .w_full()
                         .child(
                             h_flex()
                                 .id("gutter")
@@ -783,7 +785,7 @@ fn editor_with_deleted_text(
     deleted_color: Hsla,
     hunk: &HoveredHunk,
     cx: &mut ViewContext<'_, Editor>,
-) -> (u8, View<Editor>) {
+) -> (u32, View<Editor>) {
     let parent_editor = cx.view().downgrade();
     let editor = cx.new_view(|cx| {
         let multi_buffer =
@@ -885,7 +887,7 @@ fn editor_with_deleted_text(
         editor
     });
 
-    let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0 as u8);
+    let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0);
     (editor_height, editor)
 }
 

crates/editor/src/scroll.rs 🔗

@@ -307,8 +307,8 @@ impl ScrollManager {
         self.show_scrollbars
     }
 
-    pub fn autoscroll_requested(&self) -> bool {
-        self.autoscroll_request.is_some()
+    pub fn autoscroll_request(&self) -> Option<Autoscroll> {
+        self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
     }
 
     pub fn is_dragging_scrollbar(&self) -> bool {

crates/editor/src/scroll/autoscroll.rs 🔗

@@ -61,8 +61,8 @@ impl AutoscrollStrategy {
 }
 
 impl Editor {
-    pub fn autoscroll_requested(&self) -> bool {
-        self.scroll_manager.autoscroll_requested()
+    pub fn autoscroll_request(&self) -> Option<Autoscroll> {
+        self.scroll_manager.autoscroll_request()
     }
 
     pub fn autoscroll_vertically(

crates/feature_flags/src/feature_flags.rs 🔗

@@ -43,11 +43,6 @@ impl FeatureFlag for LanguageModels {
     const NAME: &'static str = "language-models";
 }
 
-pub struct GroupedDiagnostics {}
-impl FeatureFlag for GroupedDiagnostics {
-    const NAME: &'static str = "grouped-diagnostics";
-}
-
 pub struct ZedPro {}
 impl FeatureFlag for ZedPro {
     const NAME: &'static str = "zed-pro";

crates/gpui/src/window.rs 🔗

@@ -1366,11 +1366,7 @@ impl<'a> WindowContext<'a> {
 
     /// The line height associated with the current text style.
     pub fn line_height(&self) -> Pixels {
-        let rem_size = self.rem_size();
-        let text_style = self.text_style();
-        text_style
-            .line_height
-            .to_pixels(text_style.font_size, rem_size)
+        self.text_style().line_height_in_pixels(self.rem_size())
     }
 
     /// Call to prevent the default action of an event. Currently only used to prevent

crates/repl/src/outputs.rs 🔗

@@ -19,7 +19,7 @@ use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext};
 /// Given these outputs are destined for the editor with the block decorations API, all of them must report
 /// how many lines they will take up in the editor.
 pub trait LineHeight: Sized {
-    fn num_lines(&self, cx: &mut WindowContext) -> u8;
+    fn num_lines(&self, cx: &mut WindowContext) -> usize;
 }
 
 /// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
@@ -88,15 +88,9 @@ impl ImageView {
 }
 
 impl LineHeight for ImageView {
-    fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+    fn num_lines(&self, cx: &mut WindowContext) -> usize {
         let line_height = cx.line_height();
-
-        let lines = self.height as f32 / line_height.0;
-
-        if lines > u8::MAX as f32 {
-            return u8::MAX;
-        }
-        lines as u8
+        (self.height as f32 / line_height.0) as usize
     }
 }
 
@@ -257,7 +251,7 @@ impl TableView {
 }
 
 impl LineHeight for TableView {
-    fn num_lines(&self, _cx: &mut WindowContext) -> u8 {
+    fn num_lines(&self, _cx: &mut WindowContext) -> usize {
         let num_rows = match &self.table.data {
             // Rows + header
             Some(data) => data.len() + 1,
@@ -267,7 +261,7 @@ impl LineHeight for TableView {
         };
 
         let num_lines = num_rows as f32 * (1.0 + TABLE_Y_PADDING_MULTIPLE) + 1.0;
-        num_lines.ceil() as u8
+        num_lines.ceil() as usize
     }
 }
 
@@ -303,12 +297,9 @@ impl ErrorView {
 }
 
 impl LineHeight for ErrorView {
-    fn num_lines(&self, cx: &mut WindowContext) -> u8 {
-        let mut height: u8 = 1; // Start at 1 to account for the y padding
-        height = height.saturating_add(self.ename.lines().count() as u8);
-        height = height.saturating_add(self.evalue.lines().count() as u8);
-        height = height.saturating_add(self.traceback.num_lines(cx));
-        height
+    fn num_lines(&self, cx: &mut WindowContext) -> usize {
+        // Start at 1 to account for the y padding
+        1 + self.ename.lines().count() + self.evalue.lines().count() + self.traceback.num_lines(cx)
     }
 }
 
@@ -357,12 +348,12 @@ impl OutputType {
 
 impl LineHeight for OutputType {
     /// Calculates the expected number of lines
-    fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+    fn num_lines(&self, cx: &mut WindowContext) -> usize {
         match self {
             Self::Plain(stdio) => stdio.num_lines(cx),
             Self::Stream(stdio) => stdio.num_lines(cx),
             Self::Image(image) => image.num_lines(cx),
-            Self::Message(message) => message.lines().count() as u8,
+            Self::Message(message) => message.lines().count(),
             Self::Table(table) => table.num_lines(cx),
             Self::ErrorOutput(error_view) => error_view.num_lines(cx),
             Self::ClearOutputWaitMarker => 0,
@@ -572,7 +563,7 @@ impl Render for ExecutionView {
 }
 
 impl LineHeight for ExecutionView {
-    fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+    fn num_lines(&self, cx: &mut WindowContext) -> usize {
         if self.outputs.is_empty() {
             return 1; // For the status message if outputs are not there
         }
@@ -581,9 +572,7 @@ impl LineHeight for ExecutionView {
             .outputs
             .iter()
             .map(|output| output.num_lines(cx))
-            .fold(0_u8, |acc, additional_height| {
-                acc.saturating_add(additional_height)
-            })
+            .sum::<usize>()
             .max(1);
 
         let num_lines = match self.status {
@@ -597,7 +586,7 @@ impl LineHeight for ExecutionView {
 }
 
 impl LineHeight for View<ExecutionView> {
-    fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+    fn num_lines(&self, cx: &mut WindowContext) -> usize {
         self.update(cx, |execution_view, cx| execution_view.num_lines(cx))
     }
 }

crates/repl/src/session.rs 🔗

@@ -42,12 +42,10 @@ pub struct Session {
 }
 
 struct EditorBlock {
-    editor: WeakView<Editor>,
     code_range: Range<Anchor>,
     invalidation_anchor: Anchor,
     block_id: CustomBlockId,
     execution_view: View<ExecutionView>,
-    on_close: CloseBlockFn,
 }
 
 type CloseBlockFn =
@@ -84,7 +82,7 @@ impl EditorBlock {
             let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start);
             let block = BlockProperties {
                 position: code_range.end,
-                height: execution_view.num_lines(cx).saturating_add(1),
+                height: (execution_view.num_lines(cx) + 1) as u32,
                 style: BlockStyle::Sticky,
                 render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
                 disposition: BlockDisposition::Below,
@@ -95,12 +93,10 @@ impl EditorBlock {
         })?;
 
         anyhow::Ok(Self {
-            editor,
             code_range,
             invalidation_anchor,
             block_id,
             execution_view,
-            on_close,
         })
     }
 
@@ -108,24 +104,6 @@ impl EditorBlock {
         self.execution_view.update(cx, |execution_view, cx| {
             execution_view.push_message(&message.content, cx);
         });
-
-        self.editor
-            .update(cx, |editor, cx| {
-                let mut replacements = HashMap::default();
-
-                replacements.insert(
-                    self.block_id,
-                    (
-                        Some(self.execution_view.num_lines(cx).saturating_add(1)),
-                        Self::create_output_area_renderer(
-                            self.execution_view.clone(),
-                            self.on_close.clone(),
-                        ),
-                    ),
-                );
-                editor.replace_blocks(replacements, None, cx);
-            })
-            .ok();
     }
 
     fn create_output_area_renderer(

crates/repl/src/stdio.rs 🔗

@@ -96,8 +96,8 @@ impl TerminalOutput {
 }
 
 impl LineHeight for TerminalOutput {
-    fn num_lines(&self, _cx: &mut WindowContext) -> u8 {
-        self.handler.buffer.lines().count().max(1) as u8
+    fn num_lines(&self, _cx: &mut WindowContext) -> usize {
+        self.handler.buffer.lines().count().max(1)
     }
 }