zed2: Project diagnostics (#3359)

Julia created

Release Notes:

- N/A

Change summary

Cargo.lock                                              |   29 
Cargo.toml                                              |    1 
crates/command_palette2/src/command_palette.rs          |    1 
crates/diagnostics2/Cargo.toml                          |   43 
crates/diagnostics2/src/diagnostics.rs                  | 1570 +++++++++++
crates/diagnostics2/src/items.rs                        |  156 +
crates/diagnostics2/src/project_diagnostics_settings.rs |   28 
crates/diagnostics2/src/toolbar_controls.rs             |   66 
crates/editor2/src/editor.rs                            |   43 
crates/editor2/src/editor_tests.rs                      |   10 
crates/editor2/src/element.rs                           |    4 
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/app/test_context.rs                    |   26 
crates/gpui2/src/element.rs                             |    9 
crates/gpui2/src/view.rs                                |   22 
crates/gpui2/src/window.rs                              |   39 
crates/picker2/src/picker2.rs                           |    4 
crates/project_panel2/src/project_panel.rs              |    5 
crates/theme2/src/styles/players.rs                     |    2 
crates/theme2/src/theme2.rs                             |    2 
crates/ui2/src/components/icon.rs                       |   36 
crates/workspace2/src/workspace2.rs                     |    2 
crates/zed2/Cargo.toml                                  |    2 
crates/zed2/src/main.rs                                 |    1 
crates/zed2/src/zed2.rs                                 |   10 
28 files changed, 2,055 insertions(+), 90 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2614,6 +2614,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"
@@ -11554,6 +11582,7 @@ dependencies = [
  "copilot2",
  "ctor",
  "db2",
+ "diagnostics2",
  "editor2",
  "env_logger 0.9.3",
  "feature_flags2",

Cargo.toml 🔗

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

crates/command_palette2/src/command_palette.rs 🔗

@@ -117,6 +117,7 @@ impl Clone for Command {
         }
     }
 }
+
 /// Hit count for each command in the palette.
 /// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
 /// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.

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,1570 @@
+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,
+    FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent,
+    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},
+    cmp::Ordering,
+    mem,
+    ops::Range,
+    path::PathBuf,
+    sync::Arc,
+};
+pub use toolbar_controls::ToolbarControls;
+use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor};
+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);
+    cx.observe_new_views(ProjectDiagnosticsEditor::register)
+        .detach();
+}
+
+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 = Focusable<Self, Div<Self>>;
+
+    fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
+        let child = if self.path_states.is_empty() {
+            div()
+                .flex()
+                .items_center()
+                .justify_center()
+                .size_full()
+                .child(Label::new("No problems in workspace"))
+        } else {
+            div().size_full().child(self.editor.clone())
+        };
+
+        div()
+            .track_focus(&self.focus_handle)
+            .size_full()
+            .on_focus_in(Self::focus_in)
+            .on_action(Self::toggle_warnings)
+            .child(child)
+    }
+}
+
+impl ProjectDiagnosticsEditor {
+    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+        workspace.register_action(Self::deploy);
+    }
+
+    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);
+            }
+
+            EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
+                cx.emit(ItemEvent::Edit);
+            }
+
+            _ => {}
+        }
+    }
+
+    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 focus_in(&mut self, _: &FocusEvent, cx: &mut ViewContext<Self>) {
+        if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
+            self.editor.focus_handle(cx).focus(cx)
+        }
+    }
+
+    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 FocusableView for ProjectDiagnosticsEditor {
+    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for ProjectDiagnosticsEditor {
+    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>, _: &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 |_| {
+        h_stack()
+            .id("diagnostic header")
+            .gap_3()
+            .bg(gpui::red())
+            .map(|stack| {
+                let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
+                    IconElement::new(Icon::XCircle).color(TextColor::Error)
+                } else {
+                    IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)
+                };
+
+                stack.child(div().pl_8().child(icon))
+            })
+            .when_some(diagnostic.source.as_ref(), |stack, source| {
+                stack.child(Label::new(format!("{source}:")).color(TextColor::Accent))
+            })
+            .child(HighlightedLabel::new(message.clone(), highlights.clone()))
+            .when_some(diagnostic.code.as_ref(), |stack, code| {
+                stack.child(Label::new(code.clone()))
+            })
+            .render()
+    })
+}
+
+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 {
+        h_stack()
+            .bg(gpui::red())
+            .child(IconElement::new(Icon::XCircle))
+            .child(Label::new(summary.error_count.to_string()))
+            .child(IconElement::new(Icon::ExclamationTriangle))
+            .child(Label::new(summary.warning_count.to_string()))
+            .render()
+    }
+}
+
+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, VisualTestContext, 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 cx = &mut VisualTestContext::from_window(*window, cx);
+        let workspace = window.root(cx).unwrap();
+
+        // 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 cx = &mut VisualTestContext::from_window(*window, cx);
+        let workspace = window.root(cx).unwrap();
+
+        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| {
+            let settings = SettingsStore::test(cx);
+            cx.set_global(settings);
+            theme::init(theme::LoadThemes::JustBase, 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,
+                                editor_style: &editor::EditorStyle::default(),
+                            })
+                            .element_id()?
+                            .try_into()
+                            .ok()?,
+
+                        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,156 @@
+use collections::HashSet;
+use editor::{Editor, GoToDiagnostic};
+use gpui::{
+    div, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful,
+    StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView,
+};
+use language::Diagnostic;
+use lsp::LanguageServerId;
+use theme::ActiveTheme;
+use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip};
+use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
+
+use crate::ProjectDiagnosticsEditor;
+
+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 = Stateful<Self, Div<Self>>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let mut summary_row = h_stack()
+            .id(cx.entity_id())
+            .on_action(Self::go_to_next_diagnostic)
+            .rounded_md()
+            .p_1()
+            .cursor_pointer()
+            .bg(gpui::green())
+            .hover(|style| style.bg(cx.theme().colors().element_hover))
+            .active(|style| style.bg(cx.theme().colors().element_active))
+            .tooltip(|_, cx| Tooltip::text("Project Diagnostics", cx))
+            .on_click(|this, _, cx| {
+                if let Some(workspace) = this.workspace.upgrade() {
+                    workspace.update(cx, |workspace, cx| {
+                        ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
+                    })
+                }
+            });
+
+        if self.summary.error_count > 0 {
+            summary_row = summary_row.child(
+                div()
+                    .child(IconElement::new(Icon::XCircle).color(TextColor::Error))
+                    .bg(gpui::red()),
+            );
+            summary_row = summary_row.child(
+                div()
+                    .child(Label::new(self.summary.error_count.to_string()))
+                    .bg(gpui::yellow()),
+            );
+        }
+
+        if self.summary.warning_count > 0 {
+            summary_row = summary_row
+                .child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning));
+            summary_row = summary_row.child(Label::new(self.summary.warning_count.to_string()));
+        }
+
+        if self.summary.error_count == 0 && self.summary.warning_count == 0 {
+            summary_row =
+                summary_row.child(IconElement::new(Icon::Check).color(TextColor::Success));
+        }
+
+        summary_row
+    }
+}
+
+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();
+        }
+    }
+}
+
+impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
+
+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,66 @@
+use crate::ProjectDiagnosticsEditor;
+use gpui::{div, Div, EventEmitter, ParentComponent, Render, ViewContext, WeakView};
+use ui::{Icon, IconButton, Tooltip};
+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 {
+        let include_warnings = self
+            .editor
+            .as_ref()
+            .and_then(|editor| editor.upgrade())
+            .map(|editor| editor.read(cx).include_warnings)
+            .unwrap_or(false);
+
+        let tooltip = if include_warnings {
+            "Exclude Warnings"
+        } else {
+            "Include Warnings"
+        };
+
+        div().child(
+            IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
+                .tooltip(move |_, cx| Tooltip::text(tooltip, cx))
+                .on_click(|this: &mut Self, cx| {
+                    if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
+                        editor.update(cx, |editor, cx| {
+                            editor.toggle_warnings(&Default::default(), cx);
+                        });
+                    }
+                }),
+        )
+    }
+}
+
+impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
+
+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 }
+    }
+}

crates/editor2/src/editor.rs 🔗

@@ -585,7 +585,7 @@ pub enum SoftWrap {
     Column(u32),
 }
 
-#[derive(Clone)]
+#[derive(Clone, Default)]
 pub struct EditorStyle {
     pub background: Hsla,
     pub local_player: PlayerColor,
@@ -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(),
             });
@@ -5639,7 +5639,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);
         }
     }
 
@@ -5654,7 +5654,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 {
@@ -9173,7 +9173,7 @@ impl Editor {
     }
 
     fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
-        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();
@@ -9203,7 +9203,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();
     }
 }
@@ -9326,7 +9326,7 @@ impl Deref for EditorSnapshot {
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Event {
+pub enum EditorEvent {
     InputIgnored {
         text: Arc<str>,
     },
@@ -9344,8 +9344,12 @@ pub enum Event {
     },
     BufferEdited,
     Edited,
+    Reparsed,
     Focused,
     Blurred,
+    DirtyChanged,
+    Saved,
+    TitleChanged,
     DiffBaseChanged,
     SelectionsChanged {
         local: bool,
@@ -9354,6 +9358,7 @@ pub enum Event {
         local: bool,
         autoscroll: bool,
     },
+    Closed,
 }
 
 pub struct EditorFocused(pub View<Editor>);
@@ -9368,7 +9373,7 @@ pub struct EditorReleased(pub WeakView<Editor>);
 //     }
 // }
 //
-impl EventEmitter<Event> for Editor {}
+impl EventEmitter<EditorEvent> for Editor {}
 
 impl FocusableView for Editor {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
@@ -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, 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(cx, |view, cx| {
@@ -4019,7 +4019,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
     let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, 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| {
@@ -4583,7 +4583,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
     });
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
     let (view, 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(cx, |view, cx| {
@@ -4734,7 +4734,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
     let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, 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| {
@@ -6295,7 +6295,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
     });
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
     let (view, 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(cx, |view, cx| {

crates/editor2/src/element.rs 🔗

@@ -1970,6 +1970,7 @@ impl EditorElement {
                 TransformBlock::ExcerptHeader { .. } => false,
                 TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
             });
+
         let mut render_block = |block: &TransformBlock,
                                 available_space: Size<AvailableSpace>,
                                 block_id: usize,
@@ -2003,6 +2004,7 @@ impl EditorElement {
                         editor_style: &self.style,
                     })
                 }
+
                 TransformBlock::ExcerptHeader {
                     buffer,
                     range,
@@ -2046,6 +2048,7 @@ impl EditorElement {
                         }
 
                         h_stack()
+                            .id("path header block")
                             .size_full()
                             .bg(gpui::red())
                             .child(filename.unwrap_or_else(|| "untitled".to_string()))
@@ -2054,6 +2057,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 🔗

@@ -84,13 +84,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(Manager::Dismiss),
-            editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
+            editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
+            editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
     }

crates/gpui2/src/app/test_context.rs 🔗

@@ -386,6 +386,32 @@ impl<T: Send> Model<T> {
     }
 }
 
+impl<V: 'static> View<V> {
+    pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
+        use postage::prelude::{Sink as _, Stream as _};
+
+        let (mut tx, mut rx) = postage::mpsc::channel(1);
+        let mut cx = cx.app.app.borrow_mut();
+        let subscription = cx.observe(self, move |_, _| {
+            tx.try_send(()).ok();
+        });
+
+        let duration = if std::env::var("CI").is_ok() {
+            Duration::from_secs(5)
+        } else {
+            Duration::from_secs(1)
+        };
+
+        async move {
+            let notification = crate::util::timeout(duration, rx.recv())
+                .await
+                .expect("next notification timed out");
+            drop(subscription);
+            notification.expect("model dropped while test was waiting for its next notification")
+        }
+    }
+}
+
 impl<V> View<V> {
     pub fn condition<Evt>(
         &self,

crates/gpui2/src/element.rs 🔗

@@ -87,6 +87,7 @@ pub trait ParentComponent<V: 'static> {
 }
 
 trait ElementObject<V> {
+    fn element_id(&self) -> Option<ElementId>;
     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>);
     fn measure(
@@ -144,6 +145,10 @@ where
     E: Element<V>,
     E::ElementState: 'static,
 {
+    fn element_id(&self) -> Option<ElementId> {
+        self.element.element_id()
+    }
+
     fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
         let (layout_id, frame_state) = match mem::take(&mut self.phase) {
             ElementRenderPhase::Start => {
@@ -266,6 +271,10 @@ impl<V> AnyElement<V> {
         AnyElement(Box::new(RenderedElement::new(element)))
     }
 
+    pub fn element_id(&self) -> Option<ElementId> {
+        self.0.element_id()
+    }
+
     pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
         self.0.layout(view_state, cx)
     }

crates/gpui2/src/view.rs 🔗

@@ -281,17 +281,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)
+//     }
+// }
 
 pub struct RenderViewWith<C, V> {
     view: View<V>,

crates/gpui2/src/window.rs 🔗

@@ -2,14 +2,14 @@ use crate::{
     key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
     AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
     DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
-    EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
-    InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext,
-    Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path,
-    Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
-    PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
-    RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
-    Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
-    WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
+    ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model,
+    ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
+    MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
+    PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
+    Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
+    VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::HashMap;
@@ -2411,6 +2411,17 @@ impl<V: 'static + Render> WindowHandle<V> {
         }
     }
 
+    pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
+    where
+        C: Context,
+    {
+        Flatten::flatten(cx.update_window(self.any_handle, |root_view, _| {
+            root_view
+                .downcast::<V>()
+                .map_err(|_| anyhow!("the type of the window's root view has changed"))
+        }))
+    }
+
     pub fn update<C, R>(
         &self,
         cx: &mut C,
@@ -2556,6 +2567,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 🔗

@@ -143,10 +143,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 🔗

@@ -199,10 +199,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/theme2/src/styles/players.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::Hsla;
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Default)]
 pub struct PlayerColor {
     pub cursor: Hsla,
     pub background: Hsla,

crates/theme2/src/theme2.rs 🔗

@@ -130,7 +130,7 @@ impl Theme {
     }
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Default)]
 pub struct DiagnosticStyle {
     pub error: Hsla,
     pub warning: Hsla,

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,
@@ -26,12 +30,14 @@ pub enum Icon {
     ChevronUp,
     Close,
     Collab,
+    Copilot,
     Dash,
-    Exit,
+    Envelope,
     ExclamationTriangle,
+    Exit,
     File,
-    FileGeneric,
     FileDoc,
+    FileGeneric,
     FileGit,
     FileLock,
     FileRust,
@@ -44,6 +50,7 @@ pub enum Icon {
     InlayHint,
     MagicWand,
     MagnifyingGlass,
+    MailOpen,
     Maximize,
     Menu,
     MessageBubbles,
@@ -59,13 +66,6 @@ pub enum Icon {
     SplitMessage,
     Terminal,
     XCircle,
-    Copilot,
-    Envelope,
-    Bell,
-    BellOff,
-    BellRing,
-    MailOpen,
-    AtSign,
 }
 
 impl Icon {
@@ -75,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",
@@ -85,12 +89,14 @@ impl Icon {
             Icon::ChevronUp => "icons/chevron_up.svg",
             Icon::Close => "icons/x.svg",
             Icon::Collab => "icons/user_group_16.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",
@@ -103,6 +109,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",
@@ -118,13 +125,6 @@ impl Icon {
             Icon::SplitMessage => "icons/split_message.svg",
             Icon::Terminal => "icons/terminal.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/workspace2.rs 🔗

@@ -64,7 +64,7 @@ use std::{
     time::Duration,
 };
 use theme2::{ActiveTheme, ThemeSettings};
-pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
+pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
 use util::ResultExt;
 use uuid::Uuid;

crates/zed2/Cargo.toml 🔗

@@ -31,7 +31,7 @@ client = { package = "client2", path = "../client2" }
 # clock = { path = "../clock" }
 copilot = { package = "copilot2", path = "../copilot2" }
 # copilot_button = { path = "../copilot_button" }
-# diagnostics = { path = "../diagnostics" }
+diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
 db = { package = "db2", path = "../db2" }
 editor = { package="editor2", path = "../editor2" }
 # feedback = { path = "../feedback" }

crates/zed2/src/main.rs 🔗

@@ -146,6 +146,7 @@ fn main() {
         command_palette::init(cx);
         language::init(cx);
         editor::init(cx);
+        diagnostics::init(cx);
         copilot::init(
             copilot_language_server_id,
             http.clone(),

crates/zed2/src/zed2.rs 🔗

@@ -104,8 +104,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                             //         QuickActionBar::new(buffer_search_bar, workspace)
                             //     });
                             //     toolbar.add_item(quick_action_bar, cx);
-                            //     let diagnostic_editor_controls =
-                            //         cx.add_view(|_| diagnostics2::ToolbarControls::new());
+                            let diagnostic_editor_controls =
+                                cx.build_view(|_| diagnostics::ToolbarControls::new());
                             //     toolbar.add_item(diagnostic_editor_controls, cx);
                             //     let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
                             //     toolbar.add_item(project_search_bar, cx);
@@ -137,8 +137,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
 
         //     let copilot =
         //         cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
-        //     let diagnostic_summary =
-        //         cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
+        let diagnostic_summary =
+            cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
         //     let activity_indicator = activity_indicator::ActivityIndicator::new(
         //         workspace,
         //         app_state.languages.clone(),
@@ -152,7 +152,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         //     });
         //     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
         workspace.status_bar().update(cx, |status_bar, cx| {
-            // status_bar.add_left_item(diagnostic_summary, cx);
+            status_bar.add_left_item(diagnostic_summary, cx);
             // status_bar.add_left_item(activity_indicator, cx);
 
             // status_bar.add_right_item(feedback_button, cx);