Get diagnostics view almost building in the zed2 world

Julia and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                                              |   28 
Cargo.toml                                              |    1 
crates/diagnostics2/Cargo.toml                          |   43 
crates/diagnostics2/src/diagnostics.rs                  | 1653 +++++++++++
crates/diagnostics2/src/items.rs                        |  251 +
crates/diagnostics2/src/project_diagnostics_settings.rs |   28 
crates/diagnostics2/src/toolbar_controls.rs             |  123 
crates/editor2/src/editor.rs                            |   41 
crates/editor2/src/editor_tests.rs                      |   10 
crates/editor2/src/element.rs                           |    8 
crates/editor2/src/items.rs                             |   19 
crates/editor2/src/scroll.rs                            |    6 
crates/editor2/src/test/editor_test_context.rs          |    3 
crates/go_to_line2/src/go_to_line.rs                    |    6 
crates/gpui2/src/element.rs                             |    9 
crates/gpui2/src/view.rs                                |   22 
crates/gpui2/src/window.rs                              |   12 
crates/picker2/src/picker2.rs                           |    4 
crates/project_panel2/src/project_panel.rs              |    5 
crates/ui2/src/components/icon.rs                       |   38 
crates/workspace2/src/pane.rs                           |   10 
crates/workspace2/src/workspace2.rs                     |   24 
22 files changed, 2,252 insertions(+), 92 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -2573,6 +2573,34 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "diagnostics2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "collections",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "log",
+ "lsp2",
+ "postage",
+ "project2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "smallvec",
+ "theme2",
+ "ui2",
+ "unindent",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "diff"
 version = "0.1.13"

Cargo.toml πŸ”—

@@ -31,6 +31,7 @@ members = [
     "crates/refineable",
     "crates/refineable/derive_refineable",
     "crates/diagnostics",
+    "crates/diagnostics2",
     "crates/drag_and_drop",
     "crates/editor",
     "crates/feature_flags",

crates/diagnostics2/Cargo.toml πŸ”—

@@ -0,0 +1,43 @@
+[package]
+name = "diagnostics2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/diagnostics.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+language = { package = "language2", path = "../language2" }
+lsp = { package = "lsp2", path = "../lsp2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+
+log.workspace = true
+anyhow.workspace = true
+futures.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smallvec.workspace = true
+postage.workspace = true
+
+[dev-dependencies]
+client = { package = "client2", path = "../client2", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+language = { package = "language2", path = "../language2", features = ["test-support"] }
+lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
+
+serde_json.workspace = true
+unindent.workspace = true

crates/diagnostics2/src/diagnostics.rs πŸ”—

@@ -0,0 +1,1653 @@
+pub mod items;
+mod project_diagnostics_settings;
+mod toolbar_controls;
+
+use anyhow::{Context as _, Result};
+use collections::{HashMap, HashSet};
+use editor::{
+    diagnostic_block_renderer,
+    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
+    highlight_diagnostic_message,
+    scroll::autoscroll::Autoscroll,
+    Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
+};
+use futures::future::try_join_all;
+use gpui::{
+    actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter,
+    FocusHandle, Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WeakView,
+};
+use language::{
+    Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
+    SelectionGoal,
+};
+use lsp::LanguageServerId;
+use project::{DiagnosticSummary, Project, ProjectPath};
+use project_diagnostics_settings::ProjectDiagnosticsSettings;
+use settings::Settings;
+use std::{
+    any::{Any, TypeId},
+    borrow::Cow,
+    cmp::Ordering,
+    mem,
+    ops::Range,
+    path::PathBuf,
+    sync::Arc,
+};
+use theme::ThemeSettings;
+pub use toolbar_controls::ToolbarControls;
+use ui::Label;
+use util::TryFutureExt;
+use workspace::{
+    item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
+    ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
+};
+
+actions!(Deploy, ToggleWarnings);
+
+const CONTEXT_LINE_COUNT: u32 = 1;
+
+pub fn init(cx: &mut AppContext) {
+    ProjectDiagnosticsSettings::register(cx);
+    // todo!()
+    // cx.add_action(ProjectDiagnosticsEditor::deploy);
+    // cx.add_action(ProjectDiagnosticsEditor::toggle_warnings);
+    // items::init(cx);
+}
+
+struct ProjectDiagnosticsEditor {
+    project: Model<Project>,
+    workspace: WeakView<Workspace>,
+    focus_handle: FocusHandle,
+    editor: View<Editor>,
+    summary: DiagnosticSummary,
+    excerpts: Model<MultiBuffer>,
+    path_states: Vec<PathState>,
+    paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
+    current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
+    include_warnings: bool,
+    _subscriptions: Vec<Subscription>,
+}
+
+struct PathState {
+    path: ProjectPath,
+    diagnostic_groups: Vec<DiagnosticGroupState>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+struct Jump {
+    path: ProjectPath,
+    position: Point,
+    anchor: Anchor,
+}
+
+struct DiagnosticGroupState {
+    language_server_id: LanguageServerId,
+    primary_diagnostic: DiagnosticEntry<language::Anchor>,
+    primary_excerpt_ix: usize,
+    excerpts: Vec<ExcerptId>,
+    blocks: HashSet<BlockId>,
+    block_count: usize,
+}
+
+impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
+
+impl Render for ProjectDiagnosticsEditor {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().size_full().bg(gpui::red())
+    }
+}
+
+// impl View for ProjectDiagnosticsEditor {
+//     fn ui_name() -> &'static str {
+//         "ProjectDiagnosticsEditor"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         if self.path_states.is_empty() {
+//             let theme = &theme::current(cx).project_diagnostics;
+//             PaneBackdrop::new(
+//                 cx.view_id(),
+//                 Label::new("No problems in workspace", theme.empty_message.clone())
+//                     .aligned()
+//                     .contained()
+//                     .with_style(theme.container)
+//                     .into_any(),
+//             )
+//             .into_any()
+//         } else {
+//             ChildView::new(&self.editor, cx).into_any()
+//         }
+//     }
+
+//     fn focus_in(&mut self, _: AnyView, cx: &mut ViewContext<Self>) {
+//         if cx.is_self_focused() && !self.path_states.is_empty() {
+//             cx.focus(&self.editor);
+//         }
+//     }
+
+//     fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+//         let project = self.project.read(cx);
+//         json!({
+//             "project": json!({
+//                 "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
+//                 "summary": project.diagnostic_summary(cx),
+//             }),
+//             "summary": self.summary,
+//             "paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)|
+//                 (server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::<Vec<_>>())
+//             ).collect::<HashMap<_, _>>(),
+//             "current_diagnostics": self.current_diagnostics.iter().map(|(server_id, paths)|
+//                 (server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::<Vec<_>>())
+//             ).collect::<HashMap<_, _>>(),
+//             "paths_states": self.path_states.iter().map(|state|
+//                 json!({
+//                     "path": state.path.path.to_string_lossy(),
+//                     "groups": state.diagnostic_groups.iter().map(|group|
+//                         json!({
+//                             "block_count": group.blocks.len(),
+//                             "excerpt_count": group.excerpts.len(),
+//                         })
+//                     ).collect::<Vec<_>>(),
+//                 })
+//             ).collect::<Vec<_>>(),
+//         })
+//     }
+// }
+
+impl ProjectDiagnosticsEditor {
+    fn new(
+        project_handle: Model<Project>,
+        workspace: WeakView<Workspace>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let project_event_subscription =
+            cx.subscribe(&project_handle, |this, _, event, cx| match event {
+                project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
+                    log::debug!("Disk based diagnostics finished for server {language_server_id}");
+                    this.update_excerpts(Some(*language_server_id), cx);
+                }
+                project::Event::DiagnosticsUpdated {
+                    language_server_id,
+                    path,
+                } => {
+                    log::debug!("Adding path {path:?} to update for server {language_server_id}");
+                    this.paths_to_update
+                        .entry(*language_server_id)
+                        .or_default()
+                        .insert(path.clone());
+                    if this.editor.read(cx).selections.all::<usize>(cx).is_empty()
+                        && !this.is_dirty(cx)
+                    {
+                        this.update_excerpts(Some(*language_server_id), cx);
+                    }
+                }
+                _ => {}
+            });
+
+        let excerpts = cx.build_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
+        let editor = cx.build_view(|cx| {
+            let mut editor =
+                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
+            editor.set_vertical_scroll_margin(5, cx);
+            editor
+        });
+        let editor_event_subscription =
+            cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
+                Self::emit_item_event_for_editor_event(event, cx);
+                if event == &EditorEvent::Focused && this.path_states.is_empty() {
+                    cx.focus(&this.focus_handle);
+                }
+            });
+
+        let project = project_handle.read(cx);
+        let summary = project.diagnostic_summary(cx);
+        let mut this = Self {
+            project: project_handle,
+            summary,
+            workspace,
+            excerpts,
+            focus_handle: cx.focus_handle(),
+            editor,
+            path_states: Default::default(),
+            paths_to_update: HashMap::default(),
+            include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
+            current_diagnostics: HashMap::default(),
+            _subscriptions: vec![project_event_subscription, editor_event_subscription],
+        };
+        this.update_excerpts(None, cx);
+        this
+    }
+
+    fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
+        match event {
+            EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
+            EditorEvent::Saved | EditorEvent::TitleChanged => {
+                cx.emit(ItemEvent::UpdateTab);
+                cx.emit(ItemEvent::UpdateBreadcrumbs);
+            }
+            EditorEvent::Reparsed => {
+                cx.emit(ItemEvent::UpdateBreadcrumbs);
+            }
+            EditorEvent::SelectionsChanged { local } if *local => {
+                cx.emit(ItemEvent::UpdateBreadcrumbs);
+            }
+            EditorEvent::DirtyChanged => {
+                cx.emit(ItemEvent::UpdateTab);
+            }
+            EditorEvent::BufferEdited => {
+                cx.emit(ItemEvent::Edit);
+                cx.emit(ItemEvent::UpdateBreadcrumbs);
+            }
+            _ => {}
+        }
+    }
+
+    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
+        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
+            workspace.activate_item(&existing, cx);
+        } else {
+            let workspace_handle = cx.view().downgrade();
+            let diagnostics = cx.build_view(|cx| {
+                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
+            });
+            workspace.add_item(Box::new(diagnostics), cx);
+        }
+    }
+
+    fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
+        self.include_warnings = !self.include_warnings;
+        self.paths_to_update = self.current_diagnostics.clone();
+        self.update_excerpts(None, cx);
+        cx.notify();
+    }
+
+    fn update_excerpts(
+        &mut self,
+        language_server_id: Option<LanguageServerId>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        log::debug!("Updating excerpts for server {language_server_id:?}");
+        let mut paths_to_recheck = HashSet::default();
+        let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
+            .project
+            .read(cx)
+            .diagnostic_summaries(cx)
+            .fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
+                summaries.entry(server_id).or_default().insert(path);
+                summaries
+            });
+        let mut old_diagnostics = if let Some(language_server_id) = language_server_id {
+            new_summaries.retain(|server_id, _| server_id == &language_server_id);
+            self.paths_to_update.retain(|server_id, paths| {
+                if server_id == &language_server_id {
+                    paths_to_recheck.extend(paths.drain());
+                    false
+                } else {
+                    true
+                }
+            });
+            let mut old_diagnostics = HashMap::default();
+            if let Some(new_paths) = new_summaries.get(&language_server_id) {
+                if let Some(old_paths) = self
+                    .current_diagnostics
+                    .insert(language_server_id, new_paths.clone())
+                {
+                    old_diagnostics.insert(language_server_id, old_paths);
+                }
+            } else {
+                if let Some(old_paths) = self.current_diagnostics.remove(&language_server_id) {
+                    old_diagnostics.insert(language_server_id, old_paths);
+                }
+            }
+            old_diagnostics
+        } else {
+            paths_to_recheck.extend(self.paths_to_update.drain().flat_map(|(_, paths)| paths));
+            mem::replace(&mut self.current_diagnostics, new_summaries.clone())
+        };
+        for (server_id, new_paths) in new_summaries {
+            match old_diagnostics.remove(&server_id) {
+                Some(mut old_paths) => {
+                    paths_to_recheck.extend(
+                        new_paths
+                            .into_iter()
+                            .filter(|new_path| !old_paths.remove(new_path)),
+                    );
+                    paths_to_recheck.extend(old_paths);
+                }
+                None => paths_to_recheck.extend(new_paths),
+            }
+        }
+        paths_to_recheck.extend(old_diagnostics.into_iter().flat_map(|(_, paths)| paths));
+
+        if paths_to_recheck.is_empty() {
+            log::debug!("No paths to recheck for language server {language_server_id:?}");
+            return;
+        }
+        log::debug!(
+            "Rechecking {} paths for language server {:?}",
+            paths_to_recheck.len(),
+            language_server_id
+        );
+        let project = self.project.clone();
+        cx.spawn(|this, mut cx| {
+            async move {
+                let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| {
+                    let mut cx = cx.clone();
+                    let project = project.clone();
+                    let this = this.clone();
+                    async move {
+                        let buffer = project
+                            .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
+                            .await
+                            .with_context(|| format!("opening buffer for path {path:?}"))?;
+                        this.update(&mut cx, |this, cx| {
+                            this.populate_excerpts(path, language_server_id, buffer, cx);
+                        })
+                        .context("missing project")?;
+                        anyhow::Ok(())
+                    }
+                }))
+                .await
+                .context("rechecking diagnostics for paths")?;
+
+                this.update(&mut cx, |this, cx| {
+                    this.summary = this.project.read(cx).diagnostic_summary(cx);
+                    cx.emit(ItemEvent::UpdateTab);
+                    cx.emit(ItemEvent::UpdateBreadcrumbs);
+                })?;
+                anyhow::Ok(())
+            }
+            .log_err()
+        })
+        .detach();
+    }
+
+    fn populate_excerpts(
+        &mut self,
+        path: ProjectPath,
+        language_server_id: Option<LanguageServerId>,
+        buffer: Model<Buffer>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let was_empty = self.path_states.is_empty();
+        let snapshot = buffer.read(cx).snapshot();
+        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
+            Ok(ix) => ix,
+            Err(ix) => {
+                self.path_states.insert(
+                    ix,
+                    PathState {
+                        path: path.clone(),
+                        diagnostic_groups: Default::default(),
+                    },
+                );
+                ix
+            }
+        };
+
+        let mut prev_excerpt_id = if path_ix > 0 {
+            let prev_path_last_group = &self.path_states[path_ix - 1]
+                .diagnostic_groups
+                .last()
+                .unwrap();
+            prev_path_last_group.excerpts.last().unwrap().clone()
+        } else {
+            ExcerptId::min()
+        };
+
+        let path_state = &mut self.path_states[path_ix];
+        let mut groups_to_add = Vec::new();
+        let mut group_ixs_to_remove = Vec::new();
+        let mut blocks_to_add = Vec::new();
+        let mut blocks_to_remove = HashSet::default();
+        let mut first_excerpt_id = None;
+        let max_severity = if self.include_warnings {
+            DiagnosticSeverity::WARNING
+        } else {
+            DiagnosticSeverity::ERROR
+        };
+        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
+            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
+            let mut new_groups = snapshot
+                .diagnostic_groups(language_server_id)
+                .into_iter()
+                .filter(|(_, group)| {
+                    group.entries[group.primary_ix].diagnostic.severity <= max_severity
+                })
+                .peekable();
+            loop {
+                let mut to_insert = None;
+                let mut to_remove = None;
+                let mut to_keep = None;
+                match (old_groups.peek(), new_groups.peek()) {
+                    (None, None) => break,
+                    (None, Some(_)) => to_insert = new_groups.next(),
+                    (Some((_, old_group)), None) => {
+                        if language_server_id.map_or(true, |id| id == old_group.language_server_id)
+                        {
+                            to_remove = old_groups.next();
+                        } else {
+                            to_keep = old_groups.next();
+                        }
+                    }
+                    (Some((_, old_group)), Some((_, new_group))) => {
+                        let old_primary = &old_group.primary_diagnostic;
+                        let new_primary = &new_group.entries[new_group.primary_ix];
+                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
+                            Ordering::Less => {
+                                if language_server_id
+                                    .map_or(true, |id| id == old_group.language_server_id)
+                                {
+                                    to_remove = old_groups.next();
+                                } else {
+                                    to_keep = old_groups.next();
+                                }
+                            }
+                            Ordering::Equal => {
+                                to_keep = old_groups.next();
+                                new_groups.next();
+                            }
+                            Ordering::Greater => to_insert = new_groups.next(),
+                        }
+                    }
+                }
+
+                if let Some((language_server_id, group)) = to_insert {
+                    let mut group_state = DiagnosticGroupState {
+                        language_server_id,
+                        primary_diagnostic: group.entries[group.primary_ix].clone(),
+                        primary_excerpt_ix: 0,
+                        excerpts: Default::default(),
+                        blocks: Default::default(),
+                        block_count: 0,
+                    };
+                    let mut pending_range: Option<(Range<Point>, usize)> = None;
+                    let mut is_first_excerpt_for_group = true;
+                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
+                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
+                        if let Some((range, start_ix)) = &mut pending_range {
+                            if let Some(entry) = resolved_entry.as_ref() {
+                                if entry.range.start.row
+                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
+                                {
+                                    range.end = range.end.max(entry.range.end);
+                                    continue;
+                                }
+                            }
+
+                            let excerpt_start =
+                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
+                            let excerpt_end = snapshot.clip_point(
+                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
+                                Bias::Left,
+                            );
+                            let excerpt_id = excerpts
+                                .insert_excerpts_after(
+                                    prev_excerpt_id,
+                                    buffer.clone(),
+                                    [ExcerptRange {
+                                        context: excerpt_start..excerpt_end,
+                                        primary: Some(range.clone()),
+                                    }],
+                                    excerpts_cx,
+                                )
+                                .pop()
+                                .unwrap();
+
+                            prev_excerpt_id = excerpt_id.clone();
+                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
+                            group_state.excerpts.push(excerpt_id.clone());
+                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
+
+                            if is_first_excerpt_for_group {
+                                is_first_excerpt_for_group = false;
+                                let mut primary =
+                                    group.entries[group.primary_ix].diagnostic.clone();
+                                primary.message =
+                                    primary.message.split('\n').next().unwrap().to_string();
+                                group_state.block_count += 1;
+                                blocks_to_add.push(BlockProperties {
+                                    position: header_position,
+                                    height: 2,
+                                    style: BlockStyle::Sticky,
+                                    render: diagnostic_header_renderer(primary),
+                                    disposition: BlockDisposition::Above,
+                                });
+                            }
+
+                            for entry in &group.entries[*start_ix..ix] {
+                                let mut diagnostic = entry.diagnostic.clone();
+                                if diagnostic.is_primary {
+                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
+                                    diagnostic.message =
+                                        entry.diagnostic.message.split('\n').skip(1).collect();
+                                }
+
+                                if !diagnostic.message.is_empty() {
+                                    group_state.block_count += 1;
+                                    blocks_to_add.push(BlockProperties {
+                                        position: (excerpt_id.clone(), entry.range.start),
+                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
+                                        style: BlockStyle::Fixed,
+                                        render: diagnostic_block_renderer(diagnostic, true),
+                                        disposition: BlockDisposition::Below,
+                                    });
+                                }
+                            }
+
+                            pending_range.take();
+                        }
+
+                        if let Some(entry) = resolved_entry {
+                            pending_range = Some((entry.range.clone(), ix));
+                        }
+                    }
+
+                    groups_to_add.push(group_state);
+                } else if let Some((group_ix, group_state)) = to_remove {
+                    excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
+                    group_ixs_to_remove.push(group_ix);
+                    blocks_to_remove.extend(group_state.blocks.iter().copied());
+                } else if let Some((_, group)) = to_keep {
+                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
+                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
+                }
+            }
+
+            excerpts.snapshot(excerpts_cx)
+        });
+
+        self.editor.update(cx, |editor, cx| {
+            editor.remove_blocks(blocks_to_remove, None, cx);
+            let block_ids = editor.insert_blocks(
+                blocks_to_add.into_iter().map(|block| {
+                    let (excerpt_id, text_anchor) = block.position;
+                    BlockProperties {
+                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
+                        height: block.height,
+                        style: block.style,
+                        render: block.render,
+                        disposition: block.disposition,
+                    }
+                }),
+                Some(Autoscroll::fit()),
+                cx,
+            );
+
+            let mut block_ids = block_ids.into_iter();
+            for group_state in &mut groups_to_add {
+                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
+            }
+        });
+
+        for ix in group_ixs_to_remove.into_iter().rev() {
+            path_state.diagnostic_groups.remove(ix);
+        }
+        path_state.diagnostic_groups.extend(groups_to_add);
+        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
+            let range_a = &a.primary_diagnostic.range;
+            let range_b = &b.primary_diagnostic.range;
+            range_a
+                .start
+                .cmp(&range_b.start, &snapshot)
+                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
+        });
+
+        if path_state.diagnostic_groups.is_empty() {
+            self.path_states.remove(path_ix);
+        }
+
+        self.editor.update(cx, |editor, cx| {
+            let groups;
+            let mut selections;
+            let new_excerpt_ids_by_selection_id;
+            if was_empty {
+                groups = self.path_states.first()?.diagnostic_groups.as_slice();
+                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
+                selections = vec![Selection {
+                    id: 0,
+                    start: 0,
+                    end: 0,
+                    reversed: false,
+                    goal: SelectionGoal::None,
+                }];
+            } else {
+                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
+                new_excerpt_ids_by_selection_id =
+                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
+                selections = editor.selections.all::<usize>(cx);
+            }
+
+            // If any selection has lost its position, move it to start of the next primary diagnostic.
+            let snapshot = editor.snapshot(cx);
+            for selection in &mut selections {
+                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
+                    let group_ix = match groups.binary_search_by(|probe| {
+                        probe
+                            .excerpts
+                            .last()
+                            .unwrap()
+                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
+                    }) {
+                        Ok(ix) | Err(ix) => ix,
+                    };
+                    if let Some(group) = groups.get(group_ix) {
+                        let offset = excerpts_snapshot
+                            .anchor_in_excerpt(
+                                group.excerpts[group.primary_excerpt_ix].clone(),
+                                group.primary_diagnostic.range.start,
+                            )
+                            .to_offset(&excerpts_snapshot);
+                        selection.start = offset;
+                        selection.end = offset;
+                    }
+                }
+            }
+            editor.change_selections(None, cx, |s| {
+                s.select(selections);
+            });
+            Some(())
+        });
+
+        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);
+        }
+        cx.notify();
+    }
+}
+
+impl Item for ProjectDiagnosticsEditor {
+    fn focus_handle(&self) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+
+    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<T: 'static>(&self, _detail: Option<usize>, cx: &AppContext) -> AnyElement<T> {
+        render_summary(&self.summary)
+    }
+
+    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: workspace::WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<View<Self>>
+    where
+        Self: Sized,
+    {
+        Some(cx.build_view(|cx| {
+            ProjectDiagnosticsEditor::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, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
+        self.editor.save(project, cx)
+    }
+
+    fn save_as(
+        &mut self,
+        _: Model<Project>,
+        _: PathBuf,
+        _: &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 { flex: None }
+    }
+
+    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 serialized_item_kind() -> Option<&'static str> {
+        Some("diagnostics")
+    }
+
+    fn deserialize(
+        project: Model<Project>,
+        workspace: WeakView<Workspace>,
+        _workspace_id: workspace::WorkspaceId,
+        _item_id: workspace::ItemId,
+        cx: &mut ViewContext<Pane>,
+    ) -> Task<Result<View<Self>>> {
+        Task::ready(Ok(cx.build_view(|cx| Self::new(project, workspace, cx))))
+    }
+}
+
+fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
+    let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
+    Arc::new(move |cx| {
+        let settings = ThemeSettings::get_global(cx);
+        div().render()
+        // let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
+        // let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
+        //     Svg::new("icons/error.svg").with_color(theme.error_diagnostic.message.text.color)
+        // } else {
+        //     Svg::new("icons/warning.svg").with_color(theme.warning_diagnostic.message.text.color)
+        // };
+
+        // Flex::row()
+        //     .with_child(
+        //         icon.constrained()
+        //             .with_width(icon_width)
+        //             .aligned()
+        //             .contained()
+        //             .with_margin_right(cx.gutter_padding),
+        //     )
+        //     .with_children(diagnostic.source.as_ref().map(|source| {
+        //         Label::new(
+        //             format!("{source}: "),
+        //             style.source.label.clone().with_font_size(font_size),
+        //         )
+        //         .contained()
+        //         .with_style(style.message.container)
+        //         .aligned()
+        //     }))
+        //     .with_child(
+        //         Label::new(
+        //             message.clone(),
+        //             style.message.label.clone().with_font_size(font_size),
+        //         )
+        //         .with_highlights(highlights.clone())
+        //         .contained()
+        //         .with_style(style.message.container)
+        //         .aligned(),
+        //     )
+        //     .with_children(diagnostic.code.clone().map(|code| {
+        //         Label::new(code, style.code.text.clone().with_font_size(font_size))
+        //             .contained()
+        //             .with_style(style.code.container)
+        //             .aligned()
+        //     }))
+        //     .contained()
+        //     .with_style(style.container)
+        //     .with_padding_left(cx.gutter_padding)
+        //     .with_padding_right(cx.gutter_padding)
+        //     .expanded()
+        //     .into_any_named("diagnostic header")
+    })
+}
+
+pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElement<T> {
+    if summary.error_count == 0 && summary.warning_count == 0 {
+        Label::new("No problems").render()
+    } else {
+        div()
+            .bg(gpui::red())
+            .child(Label::new("TODO Show warnings/errors"))
+            .render()
+        // Flex::row()
+        //     .with_child(
+        //         Svg::new("icons/error.svg")
+        //             .with_color(text_style.color)
+        //             .constrained()
+        //             .with_width(icon_width)
+        //             .aligned()
+        //             .contained()
+        //             .with_margin_right(icon_spacing),
+        //     )
+        //     .with_child(
+        //         Label::new(
+        //             summary.error_count.to_string(),
+        //             LabelStyle {
+        //                 text: text_style.clone(),
+        //                 highlight_text: None,
+        //             },
+        //         )
+        //         .aligned(),
+        //     )
+        //     .with_child(
+        //         Svg::new("icons/warning.svg")
+        //             .with_color(text_style.color)
+        //             .constrained()
+        //             .with_width(icon_width)
+        //             .aligned()
+        //             .contained()
+        //             .with_margin_left(summary_spacing)
+        //             .with_margin_right(icon_spacing),
+        //     )
+        //     .with_child(
+        //         Label::new(
+        //             summary.warning_count.to_string(),
+        //             LabelStyle {
+        //                 text: text_style.clone(),
+        //                 highlight_text: None,
+        //             },
+        //         )
+        //         .aligned(),
+        //     )
+        //     .into_any()
+    }
+}
+
+fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
+    lhs: &DiagnosticEntry<L>,
+    rhs: &DiagnosticEntry<R>,
+    snapshot: &language::BufferSnapshot,
+) -> Ordering {
+    lhs.range
+        .start
+        .to_offset(snapshot)
+        .cmp(&rhs.range.start.to_offset(snapshot))
+        .then_with(|| {
+            lhs.range
+                .end
+                .to_offset(snapshot)
+                .cmp(&rhs.range.end.to_offset(snapshot))
+        })
+        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use editor::{
+        display_map::{BlockContext, TransformBlock},
+        DisplayPoint,
+    };
+    use gpui::{px, TestAppContext, WindowContext};
+    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
+    use project::FakeFs;
+    use serde_json::json;
+    use settings::SettingsStore;
+    use unindent::Unindent as _;
+
+    #[gpui::test]
+    async fn test_diagnostics(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/test",
+            json!({
+                "consts.rs": "
+                    const a: i32 = 'a';
+                    const b: i32 = c;
+                "
+                .unindent(),
+
+                "main.rs": "
+                    fn main() {
+                        let x = vec![];
+                        let y = vec![];
+                        a(x);
+                        b(y);
+                        // comment 1
+                        // comment 2
+                        c(y);
+                        d(x);
+                    }
+                "
+                .unindent(),
+            }),
+        )
+        .await;
+
+        let language_server_id = LanguageServerId(0);
+        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
+
+        // Create some diagnostics
+        project.update(cx, |project, cx| {
+            project
+                .update_diagnostic_entries(
+                    language_server_id,
+                    PathBuf::from("/test/main.rs"),
+                    None,
+                    vec![
+                        DiagnosticEntry {
+                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
+                            diagnostic: Diagnostic {
+                                message:
+                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
+                                        .to_string(),
+                                severity: DiagnosticSeverity::INFORMATION,
+                                is_primary: false,
+                                is_disk_based: true,
+                                group_id: 1,
+                                ..Default::default()
+                            },
+                        },
+                        DiagnosticEntry {
+                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
+                            diagnostic: Diagnostic {
+                                message:
+                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
+                                        .to_string(),
+                                severity: DiagnosticSeverity::INFORMATION,
+                                is_primary: false,
+                                is_disk_based: true,
+                                group_id: 0,
+                                ..Default::default()
+                            },
+                        },
+                        DiagnosticEntry {
+                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
+                            diagnostic: Diagnostic {
+                                message: "value moved here".to_string(),
+                                severity: DiagnosticSeverity::INFORMATION,
+                                is_primary: false,
+                                is_disk_based: true,
+                                group_id: 1,
+                                ..Default::default()
+                            },
+                        },
+                        DiagnosticEntry {
+                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
+                            diagnostic: Diagnostic {
+                                message: "value moved here".to_string(),
+                                severity: DiagnosticSeverity::INFORMATION,
+                                is_primary: false,
+                                is_disk_based: true,
+                                group_id: 0,
+                                ..Default::default()
+                            },
+                        },
+                        DiagnosticEntry {
+                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
+                            diagnostic: Diagnostic {
+                                message: "use of moved value\nvalue used here after move".to_string(),
+                                severity: DiagnosticSeverity::ERROR,
+                                is_primary: true,
+                                is_disk_based: true,
+                                group_id: 0,
+                                ..Default::default()
+                            },
+                        },
+                        DiagnosticEntry {
+                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
+                            diagnostic: Diagnostic {
+                                message: "use of moved value\nvalue used here after move".to_string(),
+                                severity: DiagnosticSeverity::ERROR,
+                                is_primary: true,
+                                is_disk_based: true,
+                                group_id: 1,
+                                ..Default::default()
+                            },
+                        },
+                    ],
+                    cx,
+                )
+                .unwrap();
+        });
+
+        // Open the project diagnostics view while there are already diagnostics.
+        let view = window.build_view(cx, |cx| {
+            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
+        });
+
+        view.next_notification(cx).await;
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (15, "collapsed context".into()),
+                    (16, "diagnostic header".into()),
+                    (25, "collapsed context".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    //
+                    // main.rs
+                    //
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n", // primary message
+                    "\n", // padding
+                    "    let x = vec![];\n",
+                    "    let y = vec![];\n",
+                    "\n", // supporting diagnostic
+                    "    a(x);\n",
+                    "    b(y);\n",
+                    "\n", // supporting diagnostic
+                    "    // comment 1\n",
+                    "    // comment 2\n",
+                    "    c(y);\n",
+                    "\n", // supporting diagnostic
+                    "    d(x);\n",
+                    "\n", // context ellipsis
+                    // diagnostic group 2
+                    "\n", // primary message
+                    "\n", // padding
+                    "fn main() {\n",
+                    "    let x = vec![];\n",
+                    "\n", // supporting diagnostic
+                    "    let y = vec![];\n",
+                    "    a(x);\n",
+                    "\n", // supporting diagnostic
+                    "    b(y);\n",
+                    "\n", // context ellipsis
+                    "    c(y);\n",
+                    "    d(x);\n",
+                    "\n", // supporting diagnostic
+                    "}"
+                )
+            );
+
+            // Cursor is at the first diagnostic
+            view.editor.update(cx, |editor, cx| {
+                assert_eq!(
+                    editor.selections.display_ranges(cx),
+                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
+                );
+            });
+        });
+
+        // Diagnostics are added for another earlier path.
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_started(language_server_id, cx);
+            project
+                .update_diagnostic_entries(
+                    language_server_id,
+                    PathBuf::from("/test/consts.rs"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
+                        diagnostic: Diagnostic {
+                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
+                            severity: DiagnosticSeverity::ERROR,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 0,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+            project.disk_based_diagnostics_finished(language_server_id, cx);
+        });
+
+        view.next_notification(cx).await;
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (7, "path header block".into()),
+                    (9, "diagnostic header".into()),
+                    (22, "collapsed context".into()),
+                    (23, "diagnostic header".into()),
+                    (32, "collapsed context".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    //
+                    // consts.rs
+                    //
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n", // primary message
+                    "\n", // padding
+                    "const a: i32 = 'a';\n",
+                    "\n", // supporting diagnostic
+                    "const b: i32 = c;\n",
+                    //
+                    // main.rs
+                    //
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n", // primary message
+                    "\n", // padding
+                    "    let x = vec![];\n",
+                    "    let y = vec![];\n",
+                    "\n", // supporting diagnostic
+                    "    a(x);\n",
+                    "    b(y);\n",
+                    "\n", // supporting diagnostic
+                    "    // comment 1\n",
+                    "    // comment 2\n",
+                    "    c(y);\n",
+                    "\n", // supporting diagnostic
+                    "    d(x);\n",
+                    "\n", // collapsed context
+                    // diagnostic group 2
+                    "\n", // primary message
+                    "\n", // filename
+                    "fn main() {\n",
+                    "    let x = vec![];\n",
+                    "\n", // supporting diagnostic
+                    "    let y = vec![];\n",
+                    "    a(x);\n",
+                    "\n", // supporting diagnostic
+                    "    b(y);\n",
+                    "\n", // context ellipsis
+                    "    c(y);\n",
+                    "    d(x);\n",
+                    "\n", // supporting diagnostic
+                    "}"
+                )
+            );
+
+            // Cursor keeps its position.
+            view.editor.update(cx, |editor, cx| {
+                assert_eq!(
+                    editor.selections.display_ranges(cx),
+                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
+                );
+            });
+        });
+
+        // Diagnostics are added to the first path
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_started(language_server_id, cx);
+            project
+                .update_diagnostic_entries(
+                    language_server_id,
+                    PathBuf::from("/test/consts.rs"),
+                    None,
+                    vec![
+                        DiagnosticEntry {
+                            range: Unclipped(PointUtf16::new(0, 15))
+                                ..Unclipped(PointUtf16::new(0, 15)),
+                            diagnostic: Diagnostic {
+                                message: "mismatched types\nexpected `usize`, found `char`"
+                                    .to_string(),
+                                severity: DiagnosticSeverity::ERROR,
+                                is_primary: true,
+                                is_disk_based: true,
+                                group_id: 0,
+                                ..Default::default()
+                            },
+                        },
+                        DiagnosticEntry {
+                            range: Unclipped(PointUtf16::new(1, 15))
+                                ..Unclipped(PointUtf16::new(1, 15)),
+                            diagnostic: Diagnostic {
+                                message: "unresolved name `c`".to_string(),
+                                severity: DiagnosticSeverity::ERROR,
+                                is_primary: true,
+                                is_disk_based: true,
+                                group_id: 1,
+                                ..Default::default()
+                            },
+                        },
+                    ],
+                    cx,
+                )
+                .unwrap();
+            project.disk_based_diagnostics_finished(language_server_id, cx);
+        });
+
+        view.next_notification(cx).await;
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (7, "collapsed context".into()),
+                    (8, "diagnostic header".into()),
+                    (13, "path header block".into()),
+                    (15, "diagnostic header".into()),
+                    (28, "collapsed context".into()),
+                    (29, "diagnostic header".into()),
+                    (38, "collapsed context".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    //
+                    // consts.rs
+                    //
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n", // primary message
+                    "\n", // padding
+                    "const a: i32 = 'a';\n",
+                    "\n", // supporting diagnostic
+                    "const b: i32 = c;\n",
+                    "\n", // context ellipsis
+                    // diagnostic group 2
+                    "\n", // primary message
+                    "\n", // padding
+                    "const a: i32 = 'a';\n",
+                    "const b: i32 = c;\n",
+                    "\n", // supporting diagnostic
+                    //
+                    // main.rs
+                    //
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n", // primary message
+                    "\n", // padding
+                    "    let x = vec![];\n",
+                    "    let y = vec![];\n",
+                    "\n", // supporting diagnostic
+                    "    a(x);\n",
+                    "    b(y);\n",
+                    "\n", // supporting diagnostic
+                    "    // comment 1\n",
+                    "    // comment 2\n",
+                    "    c(y);\n",
+                    "\n", // supporting diagnostic
+                    "    d(x);\n",
+                    "\n", // context ellipsis
+                    // diagnostic group 2
+                    "\n", // primary message
+                    "\n", // filename
+                    "fn main() {\n",
+                    "    let x = vec![];\n",
+                    "\n", // supporting diagnostic
+                    "    let y = vec![];\n",
+                    "    a(x);\n",
+                    "\n", // supporting diagnostic
+                    "    b(y);\n",
+                    "\n", // context ellipsis
+                    "    c(y);\n",
+                    "    d(x);\n",
+                    "\n", // supporting diagnostic
+                    "}"
+                )
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/test",
+            json!({
+                "main.js": "
+                    a();
+                    b();
+                    c();
+                    d();
+                    e();
+                ".unindent()
+            }),
+        )
+        .await;
+
+        let server_id_1 = LanguageServerId(100);
+        let server_id_2 = LanguageServerId(101);
+        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
+
+        let view = window.build_view(cx, |cx| {
+            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
+        });
+
+        // Two language servers start updating diagnostics
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_started(server_id_1, cx);
+            project.disk_based_diagnostics_started(server_id_2, cx);
+            project
+                .update_diagnostic_entries(
+                    server_id_1,
+                    PathBuf::from("/test/main.js"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
+                        diagnostic: Diagnostic {
+                            message: "error 1".to_string(),
+                            severity: DiagnosticSeverity::WARNING,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 1,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+        });
+
+        // The first language server finishes
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_finished(server_id_1, cx);
+        });
+
+        // Only the first language server's diagnostics are shown.
+        cx.executor().run_until_parked();
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "a();\n", //
+                    "b();",
+                )
+            );
+        });
+
+        // The second language server finishes
+        project.update(cx, |project, cx| {
+            project
+                .update_diagnostic_entries(
+                    server_id_2,
+                    PathBuf::from("/test/main.js"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
+                        diagnostic: Diagnostic {
+                            message: "warning 1".to_string(),
+                            severity: DiagnosticSeverity::ERROR,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 2,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+            project.disk_based_diagnostics_finished(server_id_2, cx);
+        });
+
+        // Both language server's diagnostics are shown.
+        cx.executor().run_until_parked();
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (6, "collapsed context".into()),
+                    (7, "diagnostic header".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "a();\n", // location
+                    "b();\n", //
+                    "\n",     // collapsed context
+                    // diagnostic group 2
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "a();\n", // context
+                    "b();\n", //
+                    "c();",   // context
+                )
+            );
+        });
+
+        // Both language servers start updating diagnostics, and the first server finishes.
+        project.update(cx, |project, cx| {
+            project.disk_based_diagnostics_started(server_id_1, cx);
+            project.disk_based_diagnostics_started(server_id_2, cx);
+            project
+                .update_diagnostic_entries(
+                    server_id_1,
+                    PathBuf::from("/test/main.js"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
+                        diagnostic: Diagnostic {
+                            message: "warning 2".to_string(),
+                            severity: DiagnosticSeverity::WARNING,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 1,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+            project
+                .update_diagnostic_entries(
+                    server_id_2,
+                    PathBuf::from("/test/main.rs"),
+                    None,
+                    vec![],
+                    cx,
+                )
+                .unwrap();
+            project.disk_based_diagnostics_finished(server_id_1, cx);
+        });
+
+        // Only the first language server's diagnostics are updated.
+        cx.executor().run_until_parked();
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (7, "collapsed context".into()),
+                    (8, "diagnostic header".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "a();\n", // location
+                    "b();\n", //
+                    "c();\n", // context
+                    "\n",     // collapsed context
+                    // diagnostic group 2
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "b();\n", // context
+                    "c();\n", //
+                    "d();",   // context
+                )
+            );
+        });
+
+        // The second language server finishes.
+        project.update(cx, |project, cx| {
+            project
+                .update_diagnostic_entries(
+                    server_id_2,
+                    PathBuf::from("/test/main.js"),
+                    None,
+                    vec![DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
+                        diagnostic: Diagnostic {
+                            message: "warning 2".to_string(),
+                            severity: DiagnosticSeverity::WARNING,
+                            is_primary: true,
+                            is_disk_based: true,
+                            group_id: 1,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+            project.disk_based_diagnostics_finished(server_id_2, cx);
+        });
+
+        // Both language servers' diagnostics are updated.
+        cx.executor().run_until_parked();
+        view.update(cx, |view, cx| {
+            assert_eq!(
+                editor_blocks(&view.editor, cx),
+                [
+                    (0, "path header block".into()),
+                    (2, "diagnostic header".into()),
+                    (7, "collapsed context".into()),
+                    (8, "diagnostic header".into()),
+                ]
+            );
+            assert_eq!(
+                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+                concat!(
+                    "\n", // filename
+                    "\n", // padding
+                    // diagnostic group 1
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "b();\n", // location
+                    "c();\n", //
+                    "d();\n", // context
+                    "\n",     // collapsed context
+                    // diagnostic group 2
+                    "\n",     // primary message
+                    "\n",     // padding
+                    "c();\n", // context
+                    "d();\n", //
+                    "e();",   // context
+                )
+            );
+        });
+    }
+
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init(cx);
+            language::init(cx);
+            client::init_settings(cx);
+            workspace::init_settings(cx);
+            Project::init_settings(cx);
+            crate::init(cx);
+        });
+    }
+
+    fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
+        editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            snapshot
+                .blocks_in_range(0..snapshot.max_point().row())
+                .enumerate()
+                .filter_map(|(ix, (row, block))| {
+                    let name = match block {
+                        TransformBlock::Custom(block) => block
+                            .render(&mut BlockContext {
+                                view_context: cx,
+                                anchor_x: px(0.),
+                                gutter_padding: px(0.),
+                                gutter_width: px(0.),
+                                line_height: px(0.),
+                                em_width: px(0.),
+                                block_id: ix,
+                            })
+                            .element_id()?
+                            .try_into()
+                            .expect("All blocks must have string ID"),
+
+                        TransformBlock::ExcerptHeader {
+                            starts_new_buffer, ..
+                        } => {
+                            if *starts_new_buffer {
+                                "path header block".into()
+                            } else {
+                                "collapsed context".into()
+                            }
+                        }
+                    };
+
+                    Some((row, name))
+                })
+                .collect()
+        })
+    }
+}

crates/diagnostics2/src/items.rs πŸ”—

@@ -0,0 +1,251 @@
+use collections::HashSet;
+use editor::{Editor, GoToDiagnostic};
+use gpui::{
+    div, serde_json, AppContext, CursorStyle, Div, Entity, EventEmitter, MouseButton, Render,
+    Styled, Subscription, View, ViewContext, WeakView,
+};
+use language::Diagnostic;
+use lsp::LanguageServerId;
+use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
+
+use crate::ProjectDiagnosticsEditor;
+
+// todo!()
+// pub fn init(cx: &mut AppContext) {
+//     cx.add_action(DiagnosticIndicator::go_to_next_diagnostic);
+// }
+
+pub struct DiagnosticIndicator {
+    summary: project::DiagnosticSummary,
+    active_editor: Option<WeakView<Editor>>,
+    workspace: WeakView<Workspace>,
+    current_diagnostic: Option<Diagnostic>,
+    in_progress_checks: HashSet<LanguageServerId>,
+    _observe_active_editor: Option<Subscription>,
+}
+
+impl Render for DiagnosticIndicator {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().size_full().bg(gpui::red())
+    }
+}
+
+impl DiagnosticIndicator {
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+        let project = workspace.project();
+        cx.subscribe(project, |this, project, event, cx| match event {
+            project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
+                this.in_progress_checks.insert(*language_server_id);
+                cx.notify();
+            }
+            project::Event::DiskBasedDiagnosticsFinished { language_server_id }
+            | project::Event::LanguageServerRemoved(language_server_id) => {
+                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.in_progress_checks.remove(language_server_id);
+                cx.notify();
+            }
+            project::Event::DiagnosticsUpdated { .. } => {
+                this.summary = project.read(cx).diagnostic_summary(cx);
+                cx.notify();
+            }
+            _ => {}
+        })
+        .detach();
+        Self {
+            summary: project.read(cx).diagnostic_summary(cx),
+            in_progress_checks: project
+                .read(cx)
+                .language_servers_running_disk_based_diagnostics()
+                .collect(),
+            active_editor: None,
+            workspace: workspace.weak_handle(),
+            current_diagnostic: None,
+            _observe_active_editor: None,
+        }
+    }
+
+    fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
+        if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
+            editor.update(cx, |editor, cx| {
+                editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
+            })
+        }
+    }
+
+    fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+        let editor = editor.read(cx);
+        let buffer = editor.buffer().read(cx);
+        let cursor_position = editor.selections.newest::<usize>(cx).head();
+        let new_diagnostic = buffer
+            .snapshot(cx)
+            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
+            .filter(|entry| !entry.range.is_empty())
+            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
+            .map(|entry| entry.diagnostic);
+        if new_diagnostic != self.current_diagnostic {
+            self.current_diagnostic = new_diagnostic;
+            cx.notify();
+        }
+    }
+}
+
+// todo: is this nessesary anymore?
+impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
+
+// impl View for DiagnosticIndicator {
+//     fn ui_name() -> &'static str {
+//         "DiagnosticIndicator"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         enum Summary {}
+//         enum Message {}
+
+//         let tooltip_style = theme::current(cx).tooltip.clone();
+//         let in_progress = !self.in_progress_checks.is_empty();
+//         let mut element = Flex::row().with_child(
+//             MouseEventHandler::new::<Summary, _>(0, cx, |state, cx| {
+//                 let theme = theme::current(cx);
+//                 let style = theme
+//                     .workspace
+//                     .status_bar
+//                     .diagnostic_summary
+//                     .style_for(state);
+
+//                 let mut summary_row = Flex::row();
+//                 if self.summary.error_count > 0 {
+//                     summary_row.add_child(
+//                         Svg::new("icons/error.svg")
+//                             .with_color(style.icon_color_error)
+//                             .constrained()
+//                             .with_width(style.icon_width)
+//                             .aligned()
+//                             .contained()
+//                             .with_margin_right(style.icon_spacing),
+//                     );
+//                     summary_row.add_child(
+//                         Label::new(self.summary.error_count.to_string(), style.text.clone())
+//                             .aligned(),
+//                     );
+//                 }
+
+//                 if self.summary.warning_count > 0 {
+//                     summary_row.add_child(
+//                         Svg::new("icons/warning.svg")
+//                             .with_color(style.icon_color_warning)
+//                             .constrained()
+//                             .with_width(style.icon_width)
+//                             .aligned()
+//                             .contained()
+//                             .with_margin_right(style.icon_spacing)
+//                             .with_margin_left(if self.summary.error_count > 0 {
+//                                 style.summary_spacing
+//                             } else {
+//                                 0.
+//                             }),
+//                     );
+//                     summary_row.add_child(
+//                         Label::new(self.summary.warning_count.to_string(), style.text.clone())
+//                             .aligned(),
+//                     );
+//                 }
+
+//                 if self.summary.error_count == 0 && self.summary.warning_count == 0 {
+//                     summary_row.add_child(
+//                         Svg::new("icons/check_circle.svg")
+//                             .with_color(style.icon_color_ok)
+//                             .constrained()
+//                             .with_width(style.icon_width)
+//                             .aligned()
+//                             .into_any_named("ok-icon"),
+//                     );
+//                 }
+
+//                 summary_row
+//                     .constrained()
+//                     .with_height(style.height)
+//                     .contained()
+//                     .with_style(if self.summary.error_count > 0 {
+//                         style.container_error
+//                     } else if self.summary.warning_count > 0 {
+//                         style.container_warning
+//                     } else {
+//                         style.container_ok
+//                     })
+//             })
+//             .with_cursor_style(CursorStyle::PointingHand)
+//             .on_click(MouseButton::Left, |_, this, cx| {
+//                 if let Some(workspace) = this.workspace.upgrade(cx) {
+//                     workspace.update(cx, |workspace, cx| {
+//                         ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
+//                     })
+//                 }
+//             })
+//             .with_tooltip::<Summary>(
+//                 0,
+//                 "Project Diagnostics",
+//                 Some(Box::new(crate::Deploy)),
+//                 tooltip_style,
+//                 cx,
+//             )
+//             .aligned()
+//             .into_any(),
+//         );
+
+//         let style = &theme::current(cx).workspace.status_bar;
+//         let item_spacing = style.item_spacing;
+
+//         if in_progress {
+//             element.add_child(
+//                 Label::new("Checking…", style.diagnostic_message.default.text.clone())
+//                     .aligned()
+//                     .contained()
+//                     .with_margin_left(item_spacing),
+//             );
+//         } else if let Some(diagnostic) = &self.current_diagnostic {
+//             let message_style = style.diagnostic_message.clone();
+//             element.add_child(
+//                 MouseEventHandler::new::<Message, _>(1, cx, |state, _| {
+//                     Label::new(
+//                         diagnostic.message.split('\n').next().unwrap().to_string(),
+//                         message_style.style_for(state).text.clone(),
+//                     )
+//                     .aligned()
+//                     .contained()
+//                     .with_margin_left(item_spacing)
+//                 })
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, |_, this, cx| {
+//                     this.go_to_next_diagnostic(&Default::default(), cx)
+//                 }),
+//             );
+//         }
+
+//         element.into_any_named("diagnostic indicator")
+//     }
+
+//     fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
+//         serde_json::json!({ "summary": self.summary })
+//     }
+// }
+
+impl StatusItemView for DiagnosticIndicator {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
+            self.active_editor = Some(editor.downgrade());
+            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
+            self.update(editor, cx);
+        } else {
+            self.active_editor = None;
+            self.current_diagnostic = None;
+            self._observe_active_editor = None;
+        }
+        cx.notify();
+    }
+}

crates/diagnostics2/src/project_diagnostics_settings.rs πŸ”—

@@ -0,0 +1,28 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Debug)]
+pub struct ProjectDiagnosticsSettings {
+    pub include_warnings: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct ProjectDiagnosticsSettingsContent {
+    include_warnings: Option<bool>,
+}
+
+impl settings::Settings for ProjectDiagnosticsSettings {
+    const KEY: Option<&'static str> = Some("diagnostics");
+    type FileContent = ProjectDiagnosticsSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _cx: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self>
+    where
+        Self: Sized,
+    {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/diagnostics2/src/toolbar_controls.rs πŸ”—

@@ -0,0 +1,123 @@
+use crate::{ProjectDiagnosticsEditor, ToggleWarnings};
+use gpui::{
+    div, Action, CursorStyle, Div, Entity, EventEmitter, MouseButton, ParentComponent, Render,
+    View, ViewContext, WeakView,
+};
+use ui::{Icon, IconButton, StyledExt};
+use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
+
+pub struct ToolbarControls {
+    editor: Option<WeakView<ProjectDiagnosticsEditor>>,
+}
+
+impl Render for ToolbarControls {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .h_flex()
+            .child(IconButton::new("toggle-warnings", Icon::Warning).on_click(|view, cx| todo!()))
+    }
+}
+
+impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
+
+// impl View for ToolbarControls {
+//     fn ui_name() -> &'static str {
+//         "ToolbarControls"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let include_warnings = self
+//             .editor
+//             .as_ref()
+//             .and_then(|editor| editor.upgrade(cx))
+//             .map(|editor| editor.read(cx).include_warnings)
+//             .unwrap_or(false);
+//         let tooltip = if include_warnings {
+//             "Exclude Warnings".into()
+//         } else {
+//             "Include Warnings".into()
+//         };
+//         Flex::row()
+//             .with_child(render_toggle_button(
+//                 0,
+//                 "icons/warning.svg",
+//                 include_warnings,
+//                 (tooltip, Some(Box::new(ToggleWarnings))),
+//                 cx,
+//                 move |this, cx| {
+//                     if let Some(editor) = this.editor.and_then(|editor| editor.upgrade(cx)) {
+//                         editor.update(cx, |editor, cx| {
+//                             editor.toggle_warnings(&Default::default(), cx)
+//                         });
+//                     }
+//                 },
+//             ))
+//             .into_any()
+//     }
+// }
+
+impl ToolbarItemView for ToolbarControls {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        _: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        if let Some(pane_item) = active_pane_item.as_ref() {
+            if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
+                self.editor = Some(editor.downgrade());
+                ToolbarItemLocation::PrimaryRight { flex: None }
+            } else {
+                ToolbarItemLocation::Hidden
+            }
+        } else {
+            ToolbarItemLocation::Hidden
+        }
+    }
+}
+
+impl ToolbarControls {
+    pub fn new() -> Self {
+        ToolbarControls { editor: None }
+    }
+}
+
+// fn render_toggle_button<
+//     F: 'static + Fn(&mut ToolbarControls, &mut EventContext<ToolbarControls>),
+// >(
+//     index: usize,
+//     icon: &'static str,
+//     toggled: bool,
+//     tooltip: (String, Option<Box<dyn Action>>),
+//     cx: &mut ViewContext<ToolbarControls>,
+//     on_click: F,
+// ) -> AnyElement<ToolbarControls> {
+//     enum Button {}
+
+//     let theme = theme::current(cx);
+//     let (tooltip_text, action) = tooltip;
+
+//     MouseEventHandler::new::<Button, _>(index, cx, |mouse_state, _| {
+//         let style = theme
+//             .workspace
+//             .toolbar
+//             .toggleable_tool
+//             .in_state(toggled)
+//             .style_for(mouse_state);
+//         Svg::new(icon)
+//             .with_color(style.color)
+//             .constrained()
+//             .with_width(style.icon_width)
+//             .aligned()
+//             .constrained()
+//             .with_width(style.button_width)
+//             .with_height(style.button_width)
+//             .contained()
+//             .with_style(style.container)
+//     })
+//     .with_cursor_style(CursorStyle::PointingHand)
+//     .on_click(MouseButton::Left, move |_, view, cx| on_click(view, cx))
+//     .with_tooltip::<Button>(index, tooltip_text, action, theme.tooltip.clone(), cx)
+//     .into_any_named("quick action bar button")
+// }

crates/editor2/src/editor.rs πŸ”—

@@ -2318,7 +2318,7 @@ impl Editor {
         }
 
         self.blink_manager.update(cx, BlinkManager::pause_blinking);
-        cx.emit(Event::SelectionsChanged { local });
+        cx.emit(EditorEvent::SelectionsChanged { local });
 
         if self.selections.disjoint_anchors().len() == 1 {
             cx.emit(SearchEvent::ActiveMatchChanged)
@@ -4242,7 +4242,7 @@ impl Editor {
 
                 self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
             }
-            cx.emit(Event::InputHandled {
+            cx.emit(EditorEvent::InputHandled {
                 utf16_range_to_replace: None,
                 text: suggestion.text.to_string().into(),
             });
@@ -5664,7 +5664,7 @@ impl Editor {
             self.request_autoscroll(Autoscroll::fit(), cx);
             self.unmark_text(cx);
             self.refresh_copilot_suggestions(true, cx);
-            cx.emit(Event::Edited);
+            cx.emit(EditorEvent::Edited);
         }
     }
 
@@ -5679,7 +5679,7 @@ impl Editor {
             self.request_autoscroll(Autoscroll::fit(), cx);
             self.unmark_text(cx);
             self.refresh_copilot_suggestions(true, cx);
-            cx.emit(Event::Edited);
+            cx.emit(EditorEvent::Edited);
         }
     }
 
@@ -8123,7 +8123,7 @@ impl Editor {
                 log::error!("unexpectedly ended a transaction that wasn't started by this editor");
             }
 
-            cx.emit(Event::Edited);
+            cx.emit(EditorEvent::Edited);
             Some(tx_id)
         } else {
             None
@@ -8711,7 +8711,7 @@ impl Editor {
                 if self.has_active_copilot_suggestion(cx) {
                     self.update_visible_copilot_suggestion(cx);
                 }
-                cx.emit(Event::BufferEdited);
+                cx.emit(EditorEvent::BufferEdited);
                 cx.emit(ItemEvent::Edit);
                 cx.emit(ItemEvent::UpdateBreadcrumbs);
                 cx.emit(SearchEvent::MatchesInvalidated);
@@ -8750,7 +8750,7 @@ impl Editor {
                 predecessor,
                 excerpts,
             } => {
-                cx.emit(Event::ExcerptsAdded {
+                cx.emit(EditorEvent::ExcerptsAdded {
                     buffer: buffer.clone(),
                     predecessor: *predecessor,
                     excerpts: excerpts.clone(),
@@ -8759,7 +8759,7 @@ impl Editor {
             }
             multi_buffer::Event::ExcerptsRemoved { ids } => {
                 self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
-                cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
+                cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
             }
             multi_buffer::Event::Reparsed => {
                 cx.emit(ItemEvent::UpdateBreadcrumbs);
@@ -8773,7 +8773,7 @@ impl Editor {
                 cx.emit(ItemEvent::UpdateTab);
                 cx.emit(ItemEvent::UpdateBreadcrumbs);
             }
-            multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged),
+            multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged),
             multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem),
             multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
@@ -9113,7 +9113,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if !self.input_enabled {
-            cx.emit(Event::InputIgnored { text: text.into() });
+            cx.emit(EditorEvent::InputIgnored { text: text.into() });
             return;
         }
         if let Some(relative_utf16_range) = relative_utf16_range {
@@ -9177,7 +9177,7 @@ impl Editor {
             // todo!()
             // let focused_event = EditorFocused(cx.handle());
             // cx.emit_global(focused_event);
-            cx.emit(Event::Focused);
+            cx.emit(EditorEvent::Focused);
         }
         if let Some(rename) = self.pending_rename.as_ref() {
             let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone();
@@ -9207,7 +9207,7 @@ impl Editor {
             .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
         self.hide_context_menu(cx);
         hide_hover(self, cx);
-        cx.emit(Event::Blurred);
+        cx.emit(EditorEvent::Blurred);
         cx.notify();
     }
 }
@@ -9330,7 +9330,7 @@ impl Deref for EditorSnapshot {
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Event {
+pub enum EditorEvent {
     InputIgnored {
         text: Arc<str>,
     },
@@ -9348,8 +9348,12 @@ pub enum Event {
     },
     BufferEdited,
     Edited,
+    Reparsed,
     Focused,
     Blurred,
+    DirtyChanged,
+    Saved,
+    TitleChanged,
     DiffBaseChanged,
     SelectionsChanged {
         local: bool,
@@ -9358,6 +9362,7 @@ pub enum Event {
         local: bool,
         autoscroll: bool,
     },
+    Closed,
 }
 
 pub struct EditorFocused(pub View<Editor>);
@@ -9372,7 +9377,7 @@ pub struct EditorReleased(pub WeakView<Editor>);
 //     }
 // }
 //
-impl EventEmitter<Event> for Editor {}
+impl EventEmitter<EditorEvent> for Editor {}
 
 impl Render for Editor {
     type Element = EditorElement;
@@ -9571,7 +9576,7 @@ impl InputHandler for Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if !self.input_enabled {
-            cx.emit(Event::InputIgnored { text: text.into() });
+            cx.emit(EditorEvent::InputIgnored { text: text.into() });
             return;
         }
 
@@ -9601,7 +9606,7 @@ impl InputHandler for Editor {
                     })
             });
 
-            cx.emit(Event::InputHandled {
+            cx.emit(EditorEvent::InputHandled {
                 utf16_range_to_replace: range_to_replace,
                 text: text.into(),
             });
@@ -9632,7 +9637,7 @@ impl InputHandler for Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if !self.input_enabled {
-            cx.emit(Event::InputIgnored { text: text.into() });
+            cx.emit(EditorEvent::InputIgnored { text: text.into() });
             return;
         }
 
@@ -9675,7 +9680,7 @@ impl InputHandler for Editor {
                     })
             });
 
-            cx.emit(Event::InputHandled {
+            cx.emit(EditorEvent::InputHandled {
                 utf16_range_to_replace: range_to_replace,
                 text: text.into(),
             });

crates/editor2/src/editor_tests.rs πŸ”—

@@ -3853,7 +3853,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
     let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
 
-    view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+    view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
     view.update(&mut cx, |view, cx| {
@@ -4020,7 +4020,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
     let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     let cx = &mut cx;
     editor
-        .condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
+        .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
         .await;
 
     editor.update(cx, |editor, cx| {
@@ -4585,7 +4585,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
     let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     let cx = &mut cx;
-    view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+    view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
     view.update(cx, |view, cx| {
@@ -4737,7 +4737,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
     let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     let cx = &mut cx;
     editor
-        .condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
     editor.update(cx, |editor, cx| {
@@ -6304,7 +6304,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
     let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     let cx = &mut cx;
-    view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+    view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
     view.update(cx, |view, cx| {

crates/editor2/src/element.rs πŸ”—

@@ -20,9 +20,9 @@ use collections::{BTreeMap, HashMap};
 use gpui::{
     point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow,
     Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
-    ElementInputHandler, Entity, EntityId, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent,
-    MouseUpEvent, ParentComponent, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun,
-    TextStyle, View, ViewContext, WindowContext,
+    ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line, MouseButton,
+    MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, ScrollWheelEvent, Size,
+    Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -2062,6 +2062,7 @@ impl EditorElement {
                         }
 
                         h_stack()
+                            .id("path header block")
                             .size_full()
                             .bg(gpui::red())
                             .child(filename.unwrap_or_else(|| "untitled".to_string()))
@@ -2070,6 +2071,7 @@ impl EditorElement {
                     } else {
                         let text_style = style.text.clone();
                         h_stack()
+                            .id("collapsed context")
                             .size_full()
                             .bg(gpui::red())
                             .child("β‹―")

crates/editor2/src/items.rs πŸ”—

@@ -1,7 +1,7 @@
 use crate::{
     editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
     movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
-    EditorSettings, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
+    EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
     NavigationData, ToPoint as _,
 };
 use anyhow::{anyhow, Context, Result};
@@ -41,11 +41,12 @@ use workspace::{
 
 pub const MAX_TAB_TITLE_LEN: usize = 24;
 
-impl FollowableEvents for Event {
+impl FollowableEvents for EditorEvent {
     fn to_follow_event(&self) -> Option<workspace::item::FollowEvent> {
         match self {
-            Event::Edited => Some(FollowEvent::Unfollow),
-            Event::SelectionsChanged { local } | Event::ScrollPositionChanged { local, .. } => {
+            EditorEvent::Edited => Some(FollowEvent::Unfollow),
+            EditorEvent::SelectionsChanged { local }
+            | EditorEvent::ScrollPositionChanged { local, .. } => {
                 if *local {
                     Some(FollowEvent::Unfollow)
                 } else {
@@ -60,7 +61,7 @@ impl FollowableEvents for Event {
 impl EventEmitter<ItemEvent> for Editor {}
 
 impl FollowableItem for Editor {
-    type FollowableEvent = Event;
+    type FollowableEvent = EditorEvent;
     fn remote_id(&self) -> Option<ViewId> {
         self.remote_id
     }
@@ -248,7 +249,7 @@ impl FollowableItem for Editor {
 
         match update {
             proto::update_view::Variant::Editor(update) => match event {
-                Event::ExcerptsAdded {
+                EditorEvent::ExcerptsAdded {
                     buffer,
                     predecessor,
                     excerpts,
@@ -269,20 +270,20 @@ impl FollowableItem for Editor {
                     }
                     true
                 }
-                Event::ExcerptsRemoved { ids } => {
+                EditorEvent::ExcerptsRemoved { ids } => {
                     update
                         .deleted_excerpts
                         .extend(ids.iter().map(ExcerptId::to_proto));
                     true
                 }
-                Event::ScrollPositionChanged { .. } => {
+                EditorEvent::ScrollPositionChanged { .. } => {
                     let scroll_anchor = self.scroll_manager.anchor();
                     update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
                     update.scroll_x = scroll_anchor.offset.x;
                     update.scroll_y = scroll_anchor.offset.y;
                     true
                 }
-                Event::SelectionsChanged { .. } => {
+                EditorEvent::SelectionsChanged { .. } => {
                     update.selections = self
                         .selections
                         .disjoint_anchors()

crates/editor2/src/scroll.rs πŸ”—

@@ -6,8 +6,8 @@ use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     hover_popover::hide_hover,
     persistence::DB,
-    Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
-    ToPoint,
+    Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
+    MultiBufferSnapshot, ToPoint,
 };
 use gpui::{point, px, AppContext, Entity, Pixels, Styled, Task, ViewContext};
 use language::{Bias, Point};
@@ -224,7 +224,7 @@ impl ScrollManager {
         cx: &mut ViewContext<Editor>,
     ) {
         self.anchor = anchor;
-        cx.emit(Event::ScrollPositionChanged { local, autoscroll });
+        cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
         self.show_scrollbar(cx);
         self.autoscroll_request.take();
         if let Some(workspace_id) = workspace_id {

crates/editor2/src/test/editor_test_context.rs πŸ”—

@@ -71,7 +71,8 @@ impl<'a> EditorTestContext<'a> {
         &self,
         predicate: impl FnMut(&Editor, &AppContext) -> bool,
     ) -> impl Future<Output = ()> {
-        self.editor.condition::<crate::Event>(&self.cx, predicate)
+        self.editor
+            .condition::<crate::EditorEvent>(&self.cx, predicate)
     }
 
     #[track_caller]

crates/go_to_line2/src/go_to_line.rs πŸ”—

@@ -83,13 +83,13 @@ impl GoToLine {
     fn on_line_editor_event(
         &mut self,
         _: View<Editor>,
-        event: &editor::Event,
+        event: &editor::EditorEvent,
         cx: &mut ViewContext<Self>,
     ) {
         match event {
             // todo!() this isn't working...
-            editor::Event::Blurred => cx.emit(ModalEvent::Dismissed),
-            editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
+            editor::EditorEvent::Blurred => cx.emit(ModalEvent::Dismissed),
+            editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
     }

crates/gpui2/src/element.rs πŸ”—

@@ -60,6 +60,7 @@ pub trait ParentComponent<V: 'static> {
 }
 
 trait ElementObject<V> {
+    fn element_id(&self) -> Option<ElementId>;
     fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
     fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
     fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
@@ -119,6 +120,10 @@ where
     E: Element<V>,
     E::ElementState: 'static,
 {
+    fn element_id(&self) -> Option<ElementId> {
+        self.element.element_id()
+    }
+
     fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
         let frame_state = if let Some(id) = self.element.element_id() {
             cx.with_element_state(id, |element_state, cx| {
@@ -271,6 +276,10 @@ impl<V> AnyElement<V> {
         AnyElement(Box::new(RenderedElement::new(element)))
     }
 
+    pub fn element_id(&self) -> Option<ElementId> {
+        self.0.element_id()
+    }
+
     pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
         self.0.initialize(view_state, cx);
     }

crates/gpui2/src/view.rs πŸ”—

@@ -269,17 +269,17 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
     }
 }
 
-impl<T, E> Render for T
-where
-    T: 'static + FnMut(&mut WindowContext) -> E,
-    E: 'static + Send + Element<T>,
-{
-    type Element = E;
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        (self)(cx)
-    }
-}
+// impl<T, E> Render for T
+// where
+//     T: 'static + FnMut(&mut WindowContext) -> E,
+//     E: 'static + Send + Element<T>,
+// {
+//     type Element = E;
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+//         (self)(cx)
+//     }
+// }
 
 mod any_view {
     use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};

crates/gpui2/src/window.rs πŸ”—

@@ -2476,6 +2476,18 @@ pub enum ElementId {
     FocusHandle(FocusId),
 }
 
+impl TryInto<SharedString> for ElementId {
+    type Error = anyhow::Error;
+
+    fn try_into(self) -> anyhow::Result<SharedString> {
+        if let ElementId::Name(name) = self {
+            Ok(name)
+        } else {
+            Err(anyhow!("element id is not string"))
+        }
+    }
+}
+
 impl From<EntityId> for ElementId {
     fn from(id: EntityId) -> Self {
         ElementId::View(id)

crates/picker2/src/picker2.rs πŸ”—

@@ -137,10 +137,10 @@ impl<D: PickerDelegate> Picker<D> {
     fn on_input_editor_event(
         &mut self,
         _: View<Editor>,
-        event: &editor::Event,
+        event: &editor::EditorEvent,
         cx: &mut ViewContext<Self>,
     ) {
-        if let editor::Event::BufferEdited = event {
+        if let editor::EditorEvent::BufferEdited = event {
             let query = self.editor.read(cx).text(cx);
             self.update_matches(query, cx);
         }

crates/project_panel2/src/project_panel.rs πŸ”—

@@ -193,10 +193,11 @@ impl ProjectPanel {
             let filename_editor = cx.build_view(|cx| Editor::single_line(cx));
 
             cx.subscribe(&filename_editor, |this, _, event, cx| match event {
-                editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
+                editor::EditorEvent::BufferEdited
+                | editor::EditorEvent::SelectionsChanged { .. } => {
                     this.autoscroll(cx);
                 }
-                editor::Event::Blurred => {
+                editor::EditorEvent::Blurred => {
                     if this
                         .edit_state
                         .as_ref()

crates/ui2/src/components/icon.rs πŸ”—

@@ -16,8 +16,12 @@ pub enum Icon {
     ArrowLeft,
     ArrowRight,
     ArrowUpRight,
+    AtSign,
     AudioOff,
     AudioOn,
+    Bell,
+    BellOff,
+    BellRing,
     Bolt,
     Check,
     ChevronDown,
@@ -25,12 +29,14 @@ pub enum Icon {
     ChevronRight,
     ChevronUp,
     Close,
+    Copilot,
     Dash,
-    Exit,
+    Envelope,
     ExclamationTriangle,
+    Exit,
     File,
-    FileGeneric,
     FileDoc,
+    FileGeneric,
     FileGit,
     FileLock,
     FileRust,
@@ -43,6 +49,7 @@ pub enum Icon {
     InlayHint,
     MagicWand,
     MagnifyingGlass,
+    MailOpen,
     Maximize,
     Menu,
     MessageBubbles,
@@ -57,14 +64,8 @@ pub enum Icon {
     Split,
     SplitMessage,
     Terminal,
+    Warning,
     XCircle,
-    Copilot,
-    Envelope,
-    Bell,
-    BellOff,
-    BellRing,
-    MailOpen,
-    AtSign,
 }
 
 impl Icon {
@@ -74,8 +75,12 @@ impl Icon {
             Icon::ArrowLeft => "icons/arrow_left.svg",
             Icon::ArrowRight => "icons/arrow_right.svg",
             Icon::ArrowUpRight => "icons/arrow_up_right.svg",
+            Icon::AtSign => "icons/at-sign.svg",
             Icon::AudioOff => "icons/speaker-off.svg",
             Icon::AudioOn => "icons/speaker-loud.svg",
+            Icon::Bell => "icons/bell.svg",
+            Icon::BellOff => "icons/bell-off.svg",
+            Icon::BellRing => "icons/bell-ring.svg",
             Icon::Bolt => "icons/bolt.svg",
             Icon::Check => "icons/check.svg",
             Icon::ChevronDown => "icons/chevron_down.svg",
@@ -83,12 +88,14 @@ impl Icon {
             Icon::ChevronRight => "icons/chevron_right.svg",
             Icon::ChevronUp => "icons/chevron_up.svg",
             Icon::Close => "icons/x.svg",
+            Icon::Copilot => "icons/copilot.svg",
             Icon::Dash => "icons/dash.svg",
-            Icon::Exit => "icons/exit.svg",
+            Icon::Envelope => "icons/feedback.svg",
             Icon::ExclamationTriangle => "icons/warning.svg",
+            Icon::Exit => "icons/exit.svg",
             Icon::File => "icons/file.svg",
-            Icon::FileGeneric => "icons/file_icons/file.svg",
             Icon::FileDoc => "icons/file_icons/book.svg",
+            Icon::FileGeneric => "icons/file_icons/file.svg",
             Icon::FileGit => "icons/file_icons/git.svg",
             Icon::FileLock => "icons/file_icons/lock.svg",
             Icon::FileRust => "icons/file_icons/rust.svg",
@@ -101,6 +108,7 @@ impl Icon {
             Icon::InlayHint => "icons/inlay_hint.svg",
             Icon::MagicWand => "icons/magic-wand.svg",
             Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
+            Icon::MailOpen => "icons/mail-open.svg",
             Icon::Maximize => "icons/maximize.svg",
             Icon::Menu => "icons/menu.svg",
             Icon::MessageBubbles => "icons/conversations.svg",
@@ -115,14 +123,8 @@ impl Icon {
             Icon::Split => "icons/split.svg",
             Icon::SplitMessage => "icons/split_message.svg",
             Icon::Terminal => "icons/terminal.svg",
+            Icon::Warning => "icons/warning.svg",
             Icon::XCircle => "icons/error.svg",
-            Icon::Copilot => "icons/copilot.svg",
-            Icon::Envelope => "icons/feedback.svg",
-            Icon::Bell => "icons/bell.svg",
-            Icon::BellOff => "icons/bell-off.svg",
-            Icon::BellRing => "icons/bell-ring.svg",
-            Icon::MailOpen => "icons/mail-open.svg",
-            Icon::AtSign => "icons/at-sign.svg",
         }
     }
 }

crates/workspace2/src/pane.rs πŸ”—

@@ -619,11 +619,11 @@ impl Pane {
         self.items.iter()
     }
 
-    //     pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
-    //         self.items
-    //             .iter()
-    //             .filter_map(|item| item.as_any().clone().downcast())
-    //     }
+    pub fn items_of_type<T: 'static>(&self) -> impl '_ + Iterator<Item = View<T>> {
+        self.items
+            .iter()
+            .filter_map(|item| item.to_any().clone().downcast().ok())
+    }
 
     pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
         self.items.get(self.active_item_index).cloned()

crates/workspace2/src/workspace2.rs πŸ”—

@@ -67,7 +67,7 @@ use std::{
     time::Duration,
 };
 use theme2::{ActiveTheme, ThemeSettings};
-pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
+pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 use ui::TextColor;
 use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
 use util::ResultExt;
@@ -1433,18 +1433,18 @@ impl Workspace {
         self.panes.iter().flat_map(|pane| pane.read(cx).items())
     }
 
-    //     pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
-    //         self.items_of_type(cx).max_by_key(|item| item.id())
-    //     }
+    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
+        self.items_of_type(cx).max_by_key(|item| item.entity_id())
+    }
 
-    //     pub fn items_of_type<'a, T: Item>(
-    //         &'a self,
-    //         cx: &'a AppContext,
-    //     ) -> impl 'a + Iterator<Item = View<T>> {
-    //         self.panes
-    //             .iter()
-    //             .flat_map(|pane| pane.read(cx).items_of_type())
-    //     }
+    pub fn items_of_type<'a, T: Item>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = View<T>> {
+        self.panes
+            .iter()
+            .flat_map(|pane| pane.read(cx).items_of_type())
+    }
 
     pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
         self.active_pane().read(cx).active_item()