Merge pull request #313 from zed-industries/polish-project-diagnostics

Antonio Scandurra created

Polish project diagnostics UX

Change summary

Cargo.lock                                  |   2 
crates/chat_panel/src/chat_panel.rs         |   2 
crates/contacts_panel/src/contacts_panel.rs |   2 
crates/diagnostics/src/diagnostics.rs       | 527 +++++++++---------
crates/diagnostics/src/items.rs             |  87 +++
crates/editor/src/display_map.rs            |  19 
crates/editor/src/display_map/fold_map.rs   |   2 
crates/editor/src/display_map/wrap_map.rs   |  11 
crates/editor/src/editor.rs                 | 268 +++++----
crates/editor/src/items.rs                  |  93 ++-
crates/editor/src/movement.rs               |   2 
crates/editor/src/multi_buffer.rs           | 315 ++++++++++
crates/editor/src/multi_buffer/anchor.rs    |   5 
crates/editor/src/test.rs                   |  27 
crates/file_finder/src/file_finder.rs       |  13 
crates/gpui/src/app.rs                      |  35 +
crates/gpui/src/elements.rs                 |  13 
crates/gpui/src/elements/expanded.rs        |  90 +++
crates/gpui/src/elements/flex.rs            |  77 --
crates/gpui/src/elements/text.rs            |  22 
crates/gpui/src/executor.rs                 | 144 +++-
crates/gpui/src/test.rs                     |  52 +
crates/language/src/buffer.rs               |  77 --
crates/language/src/diagnostic_set.rs       |  18 
crates/language/src/language.rs             |  31 
crates/language/src/proto.rs                | 134 +--
crates/language/src/tests.rs                | 146 ++---
crates/lsp/src/lsp.rs                       |  34 
crates/project/src/project.rs               | 201 +++++-
crates/project/src/worktree.rs              | 639 ++++++++++++++--------
crates/project_panel/Cargo.toml             |   1 
crates/project_panel/src/project_panel.rs   |   4 
crates/rpc/proto/zed.proto                  |  84 +-
crates/rpc/src/proto.rs                     |  12 
crates/server/src/rpc.rs                    | 206 ++++++-
crates/server/src/rpc/store.rs              |  37 +
crates/sum_tree/src/tree_map.rs             |  10 
crates/text/src/text.rs                     |   4 
crates/theme/src/theme.rs                   |  11 
crates/theme_selector/src/theme_selector.rs |   2 
crates/workspace/Cargo.toml                 |   1 
crates/workspace/src/pane.rs                | 138 ++--
crates/workspace/src/pane_group.rs          |   2 
crates/workspace/src/sidebar.rs             |  21 
crates/workspace/src/status_bar.rs          |   2 
crates/workspace/src/workspace.rs           | 398 +++++++------
crates/zed/assets/themes/_base.toml         |  25 
crates/zed/languages/rust/config.toml       |   1 
crates/zed/src/language.rs                  | 179 ------
crates/zed/src/main.rs                      |   6 
crates/zed/src/test.rs                      |   6 
crates/zed/src/zed.rs                       |  61 +-
52 files changed, 2,596 insertions(+), 1,703 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3502,6 +3502,7 @@ dependencies = [
  "project",
  "serde_json",
  "theme",
+ "util",
  "workspace",
 ]
 
@@ -5650,6 +5651,7 @@ dependencies = [
  "anyhow",
  "client",
  "clock",
+ "collections",
  "gpui",
  "language",
  "log",

crates/chat_panel/src/chat_panel.rs 🔗

@@ -233,7 +233,7 @@ impl ChatPanel {
             Empty::new().boxed()
         };
 
-        Expanded::new(1., messages).boxed()
+        Flexible::new(1., true, messages).boxed()
     }
 
     fn render_message(&self, message: &ChannelMessage) -> ElementBox {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1,37 +1,40 @@
+pub mod items;
+
 use anyhow::Result;
 use collections::{HashMap, HashSet};
 use editor::{
     context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
     display_map::{BlockDisposition, BlockId, BlockProperties},
-    BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer,
+    items::BufferItemHandle,
+    Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
 };
 use gpui::{
     action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
-    RenderContext, Task, View, ViewContext, ViewHandle,
+    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
-use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point};
+use language::{Bias, Buffer, DiagnosticEntry, Point, Selection, SelectionGoal};
 use postage::watch;
-use project::Project;
-use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc};
+use project::{Project, ProjectPath, WorktreeId};
+use std::{cmp::Ordering, mem, ops::Range, path::Path, sync::Arc};
 use util::TryFutureExt;
 use workspace::Workspace;
 
-action!(Toggle);
-action!(ClearInvalid);
+action!(Deploy);
+action!(OpenExcerpts);
 
 const CONTEXT_LINE_COUNT: u32 = 1;
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_bindings([
-        Binding::new("alt-shift-D", Toggle, None),
+        Binding::new("alt-shift-D", Deploy, Some("Workspace")),
         Binding::new(
-            "alt-shift-C",
-            ClearInvalid,
+            "alt-shift-D",
+            OpenExcerpts,
             Some("ProjectDiagnosticsEditor"),
         ),
     ]);
-    cx.add_action(ProjectDiagnosticsEditor::toggle);
-    cx.add_action(ProjectDiagnosticsEditor::clear_invalid);
+    cx.add_action(ProjectDiagnosticsEditor::deploy);
+    cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
 }
 
 type Event = editor::Event;
@@ -41,24 +44,22 @@ struct ProjectDiagnostics {
 }
 
 struct ProjectDiagnosticsEditor {
+    model: ModelHandle<ProjectDiagnostics>,
+    workspace: WeakViewHandle<Workspace>,
     editor: ViewHandle<Editor>,
     excerpts: ModelHandle<MultiBuffer>,
     path_states: Vec<(Arc<Path>, Vec<DiagnosticGroupState>)>,
+    paths_to_update: HashMap<WorktreeId, HashSet<ProjectPath>>,
     build_settings: BuildSettings,
+    settings: watch::Receiver<workspace::Settings>,
 }
 
 struct DiagnosticGroupState {
     primary_diagnostic: DiagnosticEntry<language::Anchor>,
+    primary_excerpt_ix: usize,
     excerpts: Vec<ExcerptId>,
-    blocks: HashMap<BlockId, DiagnosticBlock>,
+    blocks: HashSet<BlockId>,
     block_count: usize,
-    is_valid: bool,
-}
-
-enum DiagnosticBlock {
-    Header(Diagnostic),
-    Inline(Diagnostic),
-    Context,
 }
 
 impl ProjectDiagnostics {
@@ -81,55 +82,49 @@ impl View for ProjectDiagnosticsEditor {
     }
 
     fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-        ChildView::new(self.editor.id()).boxed()
+        if self.path_states.is_empty() {
+            let theme = &self.settings.borrow().theme.project_diagnostics;
+            Label::new(
+                "No problems detected in the project".to_string(),
+                theme.empty_message.clone(),
+            )
+            .aligned()
+            .contained()
+            .with_style(theme.container)
+            .boxed()
+        } else {
+            ChildView::new(self.editor.id()).boxed()
+        }
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        cx.focus(&self.editor);
+        if !self.path_states.is_empty() {
+            cx.focus(&self.editor);
+        }
     }
 }
 
 impl ProjectDiagnosticsEditor {
     fn new(
-        project: ModelHandle<Project>,
+        model: ModelHandle<ProjectDiagnostics>,
+        workspace: WeakViewHandle<Workspace>,
         settings: watch::Receiver<workspace::Settings>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let project_paths = project
-            .read(cx)
-            .diagnostic_summaries(cx)
-            .map(|e| e.0)
-            .collect::<Vec<_>>();
-
-        cx.spawn(|this, mut cx| {
-            let project = project.clone();
-            async move {
-                for project_path in project_paths {
-                    let buffer = project
-                        .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
-                        .await?;
-                    this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
+        let project = model.read(cx).project.clone();
+        cx.subscribe(&project, |this, _, event, cx| match event {
+            project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => {
+                if let Some(paths) = this.paths_to_update.remove(&worktree_id) {
+                    this.update_excerpts(paths, cx);
                 }
-                Result::<_, anyhow::Error>::Ok(())
             }
-        })
-        .detach();
-
-        cx.subscribe(&project, |_, project, event, cx| {
-            if let project::Event::DiagnosticsUpdated(project_path) = event {
-                let project_path = project_path.clone();
-                cx.spawn(|this, mut cx| {
-                    async move {
-                        let buffer = project
-                            .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
-                            .await?;
-                        this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx));
-                        Ok(())
-                    }
-                    .log_err()
-                })
-                .detach();
+            project::Event::DiagnosticsUpdated(path) => {
+                this.paths_to_update
+                    .entry(path.worktree_id)
+                    .or_default()
+                    .insert(path.clone());
             }
+            _ => {}
         })
         .detach();
 
@@ -139,12 +134,24 @@ impl ProjectDiagnosticsEditor {
             cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
         cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
             .detach();
-        Self {
+
+        let paths_to_update = project
+            .read(cx)
+            .diagnostic_summaries(cx)
+            .map(|e| e.0)
+            .collect();
+        let this = Self {
+            model,
+            workspace,
             excerpts,
             editor,
             build_settings,
+            settings,
             path_states: Default::default(),
-        }
+            paths_to_update: Default::default(),
+        };
+        this.update_excerpts(paths_to_update, cx);
+        this
     }
 
     #[cfg(test)]
@@ -152,41 +159,68 @@ impl ProjectDiagnosticsEditor {
         self.editor.read(cx).text(cx)
     }
 
-    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-        let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
-        workspace.add_item(diagnostics, cx);
+    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
+        if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
+            workspace.activate_item(&existing, cx);
+        } else {
+            let diagnostics =
+                cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
+            workspace.open_item(diagnostics, cx);
+        }
     }
 
-    fn clear_invalid(&mut self, _: &ClearInvalid, cx: &mut ViewContext<Self>) {
-        let mut blocks_to_delete = HashSet::default();
-        let mut excerpts_to_delete = Vec::new();
-        let mut path_ixs_to_delete = Vec::new();
-        for (ix, (_, groups)) in self.path_states.iter_mut().enumerate() {
-            groups.retain(|group| {
-                if group.is_valid {
-                    true
-                } else {
-                    blocks_to_delete.extend(group.blocks.keys().copied());
-                    excerpts_to_delete.extend(group.excerpts.iter().cloned());
-                    false
+    fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let editor = self.editor.read(cx);
+            let excerpts = self.excerpts.read(cx);
+            let mut new_selections_by_buffer = HashMap::default();
+
+            for selection in editor.local_selections::<usize>(cx) {
+                for (buffer, mut range) in
+                    excerpts.excerpted_buffers(selection.start..selection.end, cx)
+                {
+                    if selection.reversed {
+                        mem::swap(&mut range.start, &mut range.end);
+                    }
+                    new_selections_by_buffer
+                        .entry(buffer)
+                        .or_insert(Vec::new())
+                        .push(range)
                 }
-            });
-
-            if groups.is_empty() {
-                path_ixs_to_delete.push(ix);
             }
-        }
 
-        for ix in path_ixs_to_delete.into_iter().rev() {
-            self.path_states.remove(ix);
+            workspace.update(cx, |workspace, cx| {
+                for (buffer, ranges) in new_selections_by_buffer {
+                    let buffer = BufferItemHandle(buffer);
+                    workspace.activate_pane_for_item(&buffer, cx);
+                    let editor = workspace
+                        .open_item(buffer, cx)
+                        .to_any()
+                        .downcast::<Editor>()
+                        .unwrap();
+                    editor.update(cx, |editor, cx| {
+                        editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
+                    });
+                }
+            });
         }
+    }
 
-        self.excerpts.update(cx, |excerpts, cx| {
-            excerpts_to_delete.sort_unstable();
-            excerpts.remove_excerpts(&excerpts_to_delete, cx)
-        });
-        self.editor
-            .update(cx, |editor, cx| editor.remove_blocks(blocks_to_delete, cx));
+    fn update_excerpts(&self, paths: HashSet<ProjectPath>, cx: &mut ViewContext<Self>) {
+        let project = self.model.read(cx).project.clone();
+        cx.spawn(|this, mut cx| {
+            async move {
+                for path in paths {
+                    let buffer = project
+                        .update(&mut cx, |project, cx| project.open_buffer(path, cx))
+                        .await?;
+                    this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
+                }
+                Result::<_, anyhow::Error>::Ok(())
+            }
+            .log_err()
+        })
+        .detach();
     }
 
     fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
@@ -202,6 +236,7 @@ impl ProjectDiagnosticsEditor {
             }
         }
 
+        let was_empty = self.path_states.is_empty();
         let path_ix = match self
             .path_states
             .binary_search_by_key(&path.as_ref(), |e| e.0.as_ref())
@@ -225,18 +260,9 @@ impl ProjectDiagnosticsEditor {
         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_restyle = HashMap::default();
         let mut blocks_to_remove = HashSet::default();
-        let selected_excerpts = self
-            .editor
-            .read(cx)
-            .local_anchor_selections()
-            .iter()
-            .flat_map(|s| [s.start.excerpt_id().clone(), s.end.excerpt_id().clone()])
-            .collect::<HashSet<_>>();
-        let mut diagnostic_blocks = Vec::new();
         let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
-            let mut old_groups = groups.iter_mut().enumerate().peekable();
+            let mut old_groups = groups.iter().enumerate().peekable();
             let mut new_groups = snapshot
                 .diagnostic_groups()
                 .into_iter()
@@ -246,7 +272,7 @@ impl ProjectDiagnosticsEditor {
             loop {
                 let mut to_insert = None;
                 let mut to_invalidate = None;
-                let mut to_validate = None;
+                let mut to_keep = None;
                 match (old_groups.peek(), new_groups.peek()) {
                     (None, None) => break,
                     (None, Some(_)) => to_insert = new_groups.next(),
@@ -257,7 +283,7 @@ impl ProjectDiagnosticsEditor {
                         match compare_diagnostics(old_primary, new_primary, &snapshot) {
                             Ordering::Less => to_invalidate = old_groups.next(),
                             Ordering::Equal => {
-                                to_validate = old_groups.next();
+                                to_keep = old_groups.next();
                                 new_groups.next();
                             }
                             Ordering::Greater => to_insert = new_groups.next(),
@@ -268,10 +294,10 @@ impl ProjectDiagnosticsEditor {
                 if let Some(group) = to_insert {
                     let mut group_state = DiagnosticGroupState {
                         primary_diagnostic: group.entries[group.primary_ix].clone(),
+                        primary_excerpt_ix: 0,
                         excerpts: Default::default(),
                         blocks: Default::default(),
                         block_count: 0,
-                        is_valid: true,
                     };
                     let mut pending_range: Option<(Range<Point>, usize)> = None;
                     let mut is_first_excerpt_for_group = true;
@@ -309,14 +335,16 @@ impl ProjectDiagnosticsEditor {
                             if is_first_excerpt_for_group {
                                 is_first_excerpt_for_group = false;
                                 let primary = &group.entries[group.primary_ix].diagnostic;
+                                let mut header = primary.clone();
+                                header.message =
+                                    primary.message.split('\n').next().unwrap().to_string();
                                 group_state.block_count += 1;
-                                diagnostic_blocks.push(DiagnosticBlock::Header(primary.clone()));
                                 blocks_to_add.push(BlockProperties {
                                     position: header_position,
-                                    height: 2,
+                                    height: 3,
                                     render: diagnostic_header_renderer(
                                         buffer.clone(),
-                                        primary.clone(),
+                                        header,
                                         true,
                                         self.build_settings.clone(),
                                     ),
@@ -324,7 +352,6 @@ impl ProjectDiagnosticsEditor {
                                 });
                             } else {
                                 group_state.block_count += 1;
-                                diagnostic_blocks.push(DiagnosticBlock::Context);
                                 blocks_to_add.push(BlockProperties {
                                     position: header_position,
                                     height: 1,
@@ -334,17 +361,20 @@ impl ProjectDiagnosticsEditor {
                             }
 
                             for entry in &group.entries[*start_ix..ix] {
-                                if !entry.diagnostic.is_primary {
+                                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;
-                                    diagnostic_blocks
-                                        .push(DiagnosticBlock::Inline(entry.diagnostic.clone()));
                                     blocks_to_add.push(BlockProperties {
                                         position: (excerpt_id.clone(), entry.range.start.clone()),
-                                        height: entry.diagnostic.message.matches('\n').count()
-                                            as u8
-                                            + 1,
+                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
                                         render: diagnostic_block_renderer(
-                                            entry.diagnostic.clone(),
+                                            diagnostic,
                                             true,
                                             self.build_settings.clone(),
                                         ),
@@ -363,76 +393,11 @@ impl ProjectDiagnosticsEditor {
 
                     groups_to_add.push(group_state);
                 } else if let Some((group_ix, group_state)) = to_invalidate {
-                    if group_state
-                        .excerpts
-                        .iter()
-                        .any(|excerpt_id| selected_excerpts.contains(excerpt_id))
-                    {
-                        for (block_id, block) in &group_state.blocks {
-                            match block {
-                                DiagnosticBlock::Header(diagnostic) => {
-                                    blocks_to_restyle.insert(
-                                        *block_id,
-                                        diagnostic_header_renderer(
-                                            buffer.clone(),
-                                            diagnostic.clone(),
-                                            false,
-                                            self.build_settings.clone(),
-                                        ),
-                                    );
-                                }
-                                DiagnosticBlock::Inline(diagnostic) => {
-                                    blocks_to_restyle.insert(
-                                        *block_id,
-                                        diagnostic_block_renderer(
-                                            diagnostic.clone(),
-                                            false,
-                                            self.build_settings.clone(),
-                                        ),
-                                    );
-                                }
-                                DiagnosticBlock::Context => {}
-                            }
-                        }
-
-                        group_state.is_valid = false;
-                        prev_excerpt_id = group_state.excerpts.last().unwrap().clone();
-                    } else {
-                        excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
-                        group_ixs_to_remove.push(group_ix);
-                        blocks_to_remove.extend(group_state.blocks.keys().copied());
-                    }
-                } else if let Some((_, group_state)) = to_validate {
-                    for (block_id, block) in &group_state.blocks {
-                        match block {
-                            DiagnosticBlock::Header(diagnostic) => {
-                                blocks_to_restyle.insert(
-                                    *block_id,
-                                    diagnostic_header_renderer(
-                                        buffer.clone(),
-                                        diagnostic.clone(),
-                                        true,
-                                        self.build_settings.clone(),
-                                    ),
-                                );
-                            }
-                            DiagnosticBlock::Inline(diagnostic) => {
-                                blocks_to_restyle.insert(
-                                    *block_id,
-                                    diagnostic_block_renderer(
-                                        diagnostic.clone(),
-                                        true,
-                                        self.build_settings.clone(),
-                                    ),
-                                );
-                            }
-                            DiagnosticBlock::Context => {}
-                        }
-                    }
-                    group_state.is_valid = true;
-                    prev_excerpt_id = group_state.excerpts.last().unwrap().clone();
-                } else {
-                    unreachable!();
+                    excerpts.remove_excerpts(group_state.excerpts.iter(), 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();
                 }
             }
 
@@ -441,7 +406,6 @@ impl ProjectDiagnosticsEditor {
 
         self.editor.update(cx, |editor, cx| {
             editor.remove_blocks(blocks_to_remove, cx);
-            editor.replace_blocks(blocks_to_restyle, cx);
             let mut block_ids = editor
                 .insert_blocks(
                     blocks_to_add.into_iter().map(|block| {
@@ -455,8 +419,7 @@ impl ProjectDiagnosticsEditor {
                     }),
                     cx,
                 )
-                .into_iter()
-                .zip(diagnostic_blocks);
+                .into_iter();
 
             for group_state in &mut groups_to_add {
                 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
@@ -481,6 +444,58 @@ impl ProjectDiagnosticsEditor {
             self.path_states.remove(path_ix);
         }
 
+        self.editor.update(cx, |editor, cx| {
+            let groups = self.path_states.get(path_ix)?.1.as_slice();
+
+            let mut selections;
+            let new_excerpt_ids_by_selection_id;
+            if was_empty {
+                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 {
+                new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
+                selections = editor.local_selections::<usize>(cx);
+            }
+
+            // If any selection has lost its position, move it to start of the next primary diagnostic.
+            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)
+                    }) {
+                        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.clone(),
+                            )
+                            .to_offset(&excerpts_snapshot);
+                        selection.start = offset;
+                        selection.end = offset;
+                    }
+                }
+            }
+            editor.update_selections(selections, None, cx);
+            Some(())
+        });
+
+        if self.path_states.is_empty() {
+            if self.editor.is_focused(cx) {
+                cx.focus_self();
+            }
+        } else {
+            if cx.handle().is_focused(cx) {
+                cx.focus(&self.editor);
+            }
+        }
         cx.notify();
     }
 }
@@ -490,11 +505,10 @@ impl workspace::Item for ProjectDiagnostics {
 
     fn build_view(
         handle: ModelHandle<Self>,
-        settings: watch::Receiver<workspace::Settings>,
+        workspace: &Workspace,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View {
-        let project = handle.read(cx).project.clone();
-        ProjectDiagnosticsEditor::new(project, settings, cx)
+        ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)
     }
 
     fn project_path(&self) -> Option<project::ProjectPath> {
@@ -503,6 +517,12 @@ impl workspace::Item for ProjectDiagnostics {
 }
 
 impl workspace::ItemView for ProjectDiagnosticsEditor {
+    type ItemHandle = ModelHandle<ProjectDiagnostics>;
+
+    fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
+        self.model.clone()
+    }
+
     fn title(&self, _: &AppContext) -> String {
         "Project Diagnostics".to_string()
     }
@@ -511,10 +531,26 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         None
     }
 
+    fn is_dirty(&self, cx: &AppContext) -> bool {
+        self.excerpts.read(cx).read(cx).is_dirty()
+    }
+
+    fn has_conflict(&self, cx: &AppContext) -> bool {
+        self.excerpts.read(cx).read(cx).has_conflict()
+    }
+
+    fn can_save(&self, _: &AppContext) -> bool {
+        true
+    }
+
     fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
         self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
     }
 
+    fn can_save_as(&self, _: &AppContext) -> bool {
+        false
+    }
+
     fn save_as(
         &mut self,
         _: ModelHandle<project::Worktree>,
@@ -524,28 +560,12 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         unreachable!()
     }
 
-    fn is_dirty(&self, cx: &AppContext) -> bool {
-        self.excerpts.read(cx).read(cx).is_dirty()
-    }
-
-    fn has_conflict(&self, cx: &AppContext) -> bool {
-        self.excerpts.read(cx).read(cx).has_conflict()
-    }
-
     fn should_update_tab_on_event(event: &Event) -> bool {
         matches!(
             event,
             Event::Saved | Event::Dirtied | Event::FileHandleChanged
         )
     }
-
-    fn can_save(&self, _: &AppContext) -> bool {
-        true
-    }
-
-    fn can_save_as(&self, _: &AppContext) -> bool {
-        false
-    }
 }
 
 fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
@@ -570,9 +590,10 @@ fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 mod tests {
     use super::*;
     use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
+    use editor::DisplayPoint;
     use gpui::TestAppContext;
-    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry};
-    use project::FakeFs;
+    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
+    use project::{worktree, FakeFs};
     use serde_json::json;
     use std::sync::Arc;
     use unindent::Unindent as _;
@@ -580,7 +601,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_diagnostics(mut cx: TestAppContext) {
-        let settings = cx.update(WorkspaceParams::test).settings;
+        let workspace_params = cx.update(WorkspaceParams::test);
+        let settings = workspace_params.settings.clone();
         let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
         let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
@@ -629,11 +651,12 @@ mod tests {
 
         worktree.update(&mut cx, |worktree, cx| {
             worktree
-                .update_diagnostics_from_provider(
+                .update_diagnostic_entries(
                     Arc::from("/test/main.rs".as_ref()),
+                    None,
                     vec![
                         DiagnosticEntry {
-                            range: 20..21,
+                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
                             diagnostic: Diagnostic {
                                 message:
                                     "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
@@ -646,7 +669,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: 40..41,
+                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
                             diagnostic: Diagnostic {
                                 message:
                                     "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
@@ -659,7 +682,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: 58..59,
+                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
                             diagnostic: Diagnostic {
                                 message: "value moved here".to_string(),
                                 severity: DiagnosticSeverity::INFORMATION,
@@ -670,7 +693,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: 68..69,
+                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
                             diagnostic: Diagnostic {
                                 message: "value moved here".to_string(),
                                 severity: DiagnosticSeverity::INFORMATION,
@@ -681,9 +704,9 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: 112..113,
+                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
                             diagnostic: Diagnostic {
-                                message: "use of moved value".to_string(),
+                                message: "use of moved value\nvalue used here after move".to_string(),
                                 severity: DiagnosticSeverity::ERROR,
                                 is_primary: true,
                                 is_disk_based: true,
@@ -692,20 +715,9 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: 112..113,
+                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
                             diagnostic: Diagnostic {
-                                message: "value used here after move".to_string(),
-                                severity: DiagnosticSeverity::INFORMATION,
-                                is_primary: false,
-                                is_disk_based: true,
-                                group_id: 0,
-                                ..Default::default()
-                            },
-                        },
-                        DiagnosticEntry {
-                            range: 122..123,
-                            diagnostic: Diagnostic {
-                                message: "use of moved value".to_string(),
+                                message: "use of moved value\nvalue used here after move".to_string(),
                                 severity: DiagnosticSeverity::ERROR,
                                 is_primary: true,
                                 is_disk_based: true,
@@ -713,25 +725,17 @@ mod tests {
                                 ..Default::default()
                             },
                         },
-                        DiagnosticEntry {
-                            range: 122..123,
-                            diagnostic: Diagnostic {
-                                message: "value used here after move".to_string(),
-                                severity: DiagnosticSeverity::INFORMATION,
-                                is_primary: false,
-                                is_disk_based: true,
-                                group_id: 1,
-                                ..Default::default()
-                            },
-                        },
                     ],
                     cx,
                 )
                 .unwrap();
         });
 
-        let view = cx.add_view(Default::default(), |cx| {
-            ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
+        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
+        let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
+
+        let view = cx.add_view(0, |cx| {
+            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), settings, cx)
         });
 
         view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
@@ -746,6 +750,7 @@ mod tests {
                     //
                     // main.rs, diagnostic group 1
                     //
+                    "\n", // padding
                     "\n", // primary message
                     "\n", // filename
                     "    let x = vec![];\n",
@@ -762,6 +767,7 @@ mod tests {
                     //
                     // main.rs, diagnostic group 2
                     //
+                    "\n", // padding
                     "\n", // primary message
                     "\n", // filename
                     "fn main() {\n",
@@ -778,39 +784,35 @@ mod tests {
                     "}"
                 )
             );
+
+            view.editor.update(cx, |editor, cx| {
+                assert_eq!(
+                    editor.selected_display_ranges(cx),
+                    [DisplayPoint::new(11, 6)..DisplayPoint::new(11, 6)]
+                );
+            });
         });
 
         worktree.update(&mut cx, |worktree, cx| {
             worktree
-                .update_diagnostics_from_provider(
+                .update_diagnostic_entries(
                     Arc::from("/test/a.rs".as_ref()),
-                    vec![
-                        DiagnosticEntry {
-                            range: 15..15,
-                            diagnostic: Diagnostic {
-                                message: "mismatched types".to_string(),
-                                severity: DiagnosticSeverity::ERROR,
-                                is_primary: true,
-                                is_disk_based: true,
-                                group_id: 0,
-                                ..Default::default()
-                            },
+                    None,
+                    vec![DiagnosticEntry {
+                        range: PointUtf16::new(0, 15)..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: 15..15,
-                            diagnostic: Diagnostic {
-                                message: "expected `usize`, found `char`".to_string(),
-                                severity: DiagnosticSeverity::INFORMATION,
-                                is_primary: false,
-                                is_disk_based: true,
-                                group_id: 0,
-                                ..Default::default()
-                            },
-                        },
-                    ],
+                    }],
                     cx,
                 )
                 .unwrap();
+            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
         });
 
         view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
@@ -825,6 +827,7 @@ mod tests {
                     //
                     // a.rs
                     //
+                    "\n", // padding
                     "\n", // primary message
                     "\n", // filename
                     "const a: i32 = 'a';\n",
@@ -833,6 +836,7 @@ mod tests {
                     //
                     // main.rs, diagnostic group 1
                     //
+                    "\n", // padding
                     "\n", // primary message
                     "\n", // filename
                     "    let x = vec![];\n",
@@ -849,6 +853,7 @@ mod tests {
                     //
                     // main.rs, diagnostic group 2
                     //
+                    "\n", // padding
                     "\n", // primary message
                     "\n", // filename
                     "fn main() {\n",

crates/diagnostics/src/items.rs 🔗

@@ -0,0 +1,87 @@
+use gpui::{
+    elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
+};
+use postage::watch;
+use project::Project;
+use std::fmt::Write;
+use workspace::{Settings, StatusItemView};
+
+pub struct DiagnosticSummary {
+    settings: watch::Receiver<Settings>,
+    summary: project::DiagnosticSummary,
+    in_progress: bool,
+}
+
+impl DiagnosticSummary {
+    pub fn new(
+        project: &ModelHandle<Project>,
+        settings: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        cx.subscribe(project, |this, project, event, cx| match event {
+            project::Event::DiskBasedDiagnosticsUpdated { .. } => {
+                this.summary = project.read(cx).diagnostic_summary(cx);
+                cx.notify();
+            }
+            project::Event::DiskBasedDiagnosticsStarted => {
+                this.in_progress = true;
+                cx.notify();
+            }
+            project::Event::DiskBasedDiagnosticsFinished => {
+                this.in_progress = false;
+                cx.notify();
+            }
+            _ => {}
+        })
+        .detach();
+        Self {
+            settings,
+            summary: project.read(cx).diagnostic_summary(cx),
+            in_progress: project.read(cx).is_running_disk_based_diagnostics(),
+        }
+    }
+}
+
+impl Entity for DiagnosticSummary {
+    type Event = ();
+}
+
+impl View for DiagnosticSummary {
+    fn ui_name() -> &'static str {
+        "DiagnosticSummary"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        enum Tag {}
+
+        let theme = &self.settings.borrow().theme.project_diagnostics;
+        let mut message = String::new();
+        if self.in_progress {
+            message.push_str("Checking... ");
+        }
+        write!(
+            message,
+            "Errors: {}, Warnings: {}",
+            self.summary.error_count, self.summary.warning_count
+        )
+        .unwrap();
+        MouseEventHandler::new::<Tag, _, _, _>(0, cx, |_, _| {
+            Label::new(message, theme.status_bar_item.text.clone())
+                .contained()
+                .with_style(theme.status_bar_item.container)
+                .boxed()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(|cx| cx.dispatch_action(crate::Deploy))
+        .boxed()
+    }
+}
+
+impl StatusItemView for DiagnosticSummary {
+    fn set_active_pane_item(
+        &mut self,
+        _: Option<&dyn workspace::ItemViewHandle>,
+        _: &mut ViewContext<Self>,
+    ) {
+    }
+}

crates/editor/src/display_map.rs 🔗

@@ -199,7 +199,10 @@ impl DisplaySnapshot {
 
     pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            point.column = 0;
+            let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Left);
+            *fold_point.column_mut() = 0;
+            point = fold_point.to_buffer_point(&self.folds_snapshot);
+
             let mut display_point = self.point_to_display_point(point, Bias::Left);
             *display_point.column_mut() = 0;
             let next_point = self.display_point_to_point(display_point, Bias::Left);
@@ -212,7 +215,10 @@ impl DisplaySnapshot {
 
     pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            point.column = self.buffer_snapshot.line_len(point.row);
+            let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Right);
+            *fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row());
+            point = fold_point.to_buffer_point(&self.folds_snapshot);
+
             let mut display_point = self.point_to_display_point(point, Bias::Right);
             *display_point.column_mut() = self.line_len(display_point.row());
             let next_point = self.display_point_to_point(display_point, Bias::Right);
@@ -446,10 +452,11 @@ impl ToDisplayPoint for Anchor {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{movement, test::*};
-    use gpui::{color::Color, elements::*, MutableAppContext};
+    use crate::movement;
+    use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
     use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
     use rand::{prelude::*, Rng};
+    use smol::stream::StreamExt;
     use std::{env, sync::Arc};
     use theme::SyntaxTheme;
     use util::test::sample_text;
@@ -493,7 +500,7 @@ mod tests {
         let map = cx.add_model(|cx| {
             DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
         });
-        let (_observer, notifications) = Observer::new(&map, &mut cx);
+        let mut notifications = observe(&map, &mut cx);
         let mut fold_count = 0;
         let mut blocks = Vec::new();
 
@@ -589,7 +596,7 @@ mod tests {
             }
 
             if map.read_with(&cx, |map, cx| map.is_rewrapping(cx)) {
-                notifications.recv().await.unwrap();
+                notifications.next().await.unwrap();
             }
 
             let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));

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

@@ -37,7 +37,6 @@ impl FoldPoint {
         &mut self.0.row
     }
 
-    #[cfg(test)]
     pub fn column_mut(&mut self) -> &mut u32 {
         &mut self.0.column
     }
@@ -549,7 +548,6 @@ impl FoldSnapshot {
         FoldOffset(self.transforms.summary().output.bytes)
     }
 
-    #[cfg(test)]
     pub fn line_len(&self, row: u32) -> u32 {
         let line_start = FoldPoint::new(row, 0).to_offset(self).0;
         let line_end = if row >= self.max_point().row() {

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

@@ -1014,11 +1014,12 @@ mod tests {
     use super::*;
     use crate::{
         display_map::{fold_map::FoldMap, tab_map::TabMap},
-        test::Observer,
         MultiBuffer,
     };
+    use gpui::test::observe;
     use language::RandomCharIter;
     use rand::prelude::*;
+    use smol::stream::StreamExt;
     use std::{cmp, env};
     use text::Rope;
 
@@ -1072,10 +1073,10 @@ mod tests {
 
         let (wrap_map, _) =
             cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx));
-        let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
+        let mut notifications = observe(&wrap_map, &mut cx);
 
         if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
-            notifications.recv().await.unwrap();
+            notifications.next().await.unwrap();
         }
 
         let (initial_snapshot, _) = wrap_map.update(&mut cx, |map, cx| {
@@ -1148,7 +1149,7 @@ mod tests {
             if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
                 log::info!("Waiting for wrapping to finish");
                 while wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
-                    notifications.recv().await.unwrap();
+                    notifications.next().await.unwrap();
                 }
                 wrap_map.read_with(&cx, |map, _| assert!(map.pending_edits.is_empty()));
             }
@@ -1236,7 +1237,7 @@ mod tests {
         if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
             log::info!("Waiting for wrapping to finish");
             while wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
-                notifications.recv().await.unwrap();
+                notifications.next().await.unwrap();
             }
         }
         wrap_map.read_with(&cx, |map, _| assert!(map.pending_edits.is_empty()));

crates/editor/src/editor.rs 🔗

@@ -28,8 +28,8 @@ use language::{
     BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal,
     TransactionId,
 };
-pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer};
-use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot, ToOffset, ToPoint};
+pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint};
+use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot};
 use postage::watch;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
@@ -46,7 +46,7 @@ use sum_tree::Bias;
 use text::rope::TextDimension;
 use theme::{DiagnosticStyle, EditorStyle};
 use util::post_inc;
-use workspace::{EntryOpener, Workspace};
+use workspace::{PathOpener, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
@@ -111,8 +111,8 @@ action!(FoldSelectedRanges);
 action!(Scroll, Vector2F);
 action!(Select, SelectPhase);
 
-pub fn init(cx: &mut MutableAppContext, entry_openers: &mut Vec<Box<dyn EntryOpener>>) {
-    entry_openers.push(Box::new(items::BufferOpener));
+pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpener>>) {
+    path_openers.push(Box::new(items::BufferOpener));
     cx.add_bindings(vec![
         Binding::new("escape", Cancel, Some("Editor")),
         Binding::new("backspace", Backspace, Some("Editor")),
@@ -365,7 +365,7 @@ pub struct Editor {
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
     scroll_position: Vector2F,
-    scroll_top_anchor: Anchor,
+    scroll_top_anchor: Option<Anchor>,
     autoscroll_request: Option<Autoscroll>,
     build_settings: BuildSettings,
     focused: bool,
@@ -383,7 +383,7 @@ pub struct EditorSnapshot {
     pub placeholder_text: Option<Arc<str>>,
     is_focused: bool,
     scroll_position: Vector2F,
-    scroll_top_anchor: Anchor,
+    scroll_top_anchor: Option<Anchor>,
 }
 
 struct PendingSelection {
@@ -495,7 +495,7 @@ impl Editor {
             active_diagnostics: None,
             build_settings,
             scroll_position: Vector2F::zero(),
-            scroll_top_anchor: Anchor::min(),
+            scroll_top_anchor: None,
             autoscroll_request: None,
             focused: false,
             show_local_cursors: false,
@@ -524,8 +524,7 @@ impl Editor {
         let buffer = cx.add_model(|cx| {
             Buffer::new(0, "", cx).with_language(Some(language::PLAIN_TEXT.clone()), None, cx)
         });
-        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        workspace.add_item(BufferItemHandle(buffer), cx);
+        workspace.open_item(BufferItemHandle(buffer), cx);
     }
 
     pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
@@ -565,15 +564,22 @@ impl Editor {
 
     pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
         let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let scroll_top_buffer_offset =
-            DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right);
-        self.scroll_top_anchor = map
-            .buffer_snapshot
-            .anchor_at(scroll_top_buffer_offset, Bias::Right);
-        self.scroll_position = vec2f(
-            scroll_position.x(),
-            scroll_position.y() - self.scroll_top_anchor.to_display_point(&map).row() as f32,
-        );
+
+        if scroll_position.y() == 0. {
+            self.scroll_top_anchor = None;
+            self.scroll_position = scroll_position;
+        } else {
+            let scroll_top_buffer_offset =
+                DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right);
+            let anchor = map
+                .buffer_snapshot
+                .anchor_at(scroll_top_buffer_offset, Bias::Right);
+            self.scroll_position = vec2f(
+                scroll_position.x(),
+                scroll_position.y() - anchor.to_display_point(&map).row() as f32,
+            );
+            self.scroll_top_anchor = Some(anchor);
+        }
 
         cx.notify();
     }
@@ -1049,6 +1055,45 @@ impl Editor {
         }
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &mut MutableAppContext,
+    ) -> Vec<Range<D>> {
+        self.local_selections::<D>(cx)
+            .iter()
+            .map(|s| {
+                if s.reversed {
+                    s.end.clone()..s.start.clone()
+                } else {
+                    s.start.clone()..s.end.clone()
+                }
+            })
+            .collect()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
+        let display_map = self
+            .display_map
+            .update(cx, |display_map, cx| display_map.snapshot(cx));
+        self.selections
+            .iter()
+            .chain(
+                self.pending_selection
+                    .as_ref()
+                    .map(|pending| &pending.selection),
+            )
+            .map(|s| {
+                if s.reversed {
+                    s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
+                } else {
+                    s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
+                }
+            })
+            .collect()
+    }
+
     pub fn select_ranges<I, T>(
         &mut self,
         ranges: I,
@@ -1059,7 +1104,7 @@ impl Editor {
         T: ToOffset,
     {
         let buffer = self.buffer.read(cx).snapshot(cx);
-        let selections = ranges
+        let mut selections = ranges
             .into_iter()
             .map(|range| {
                 let mut start = range.start.to_offset(&buffer);
@@ -1078,7 +1123,8 @@ impl Editor {
                     goal: SelectionGoal::None,
                 }
             })
-            .collect();
+            .collect::<Vec<_>>();
+        selections.sort_unstable_by_key(|s| s.start);
         self.update_selections(selections, autoscroll, cx);
     }
 
@@ -1564,7 +1610,6 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = self.buffer.read(cx).snapshot(cx);
 
-        let mut row_delta = 0;
         let mut new_cursors = Vec::new();
         let mut edit_ranges = Vec::new();
         let mut selections = selections.iter().peekable();
@@ -1590,7 +1635,7 @@ impl Editor {
                 // If there's a line after the range, delete the \n from the end of the row range
                 // and position the cursor on the next line.
                 edit_end = Point::new(rows.end, 0).to_offset(&buffer);
-                cursor_buffer_row = rows.start;
+                cursor_buffer_row = rows.end;
             } else {
                 // If there isn't a line after the range, delete the \n from the line before the
                 // start of the row range and position the cursor there.
@@ -1599,29 +1644,35 @@ impl Editor {
                 cursor_buffer_row = rows.start.saturating_sub(1);
             }
 
-            let mut cursor =
-                Point::new(cursor_buffer_row - row_delta, 0).to_display_point(&display_map);
+            let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map);
             *cursor.column_mut() =
                 cmp::min(goal_display_column, display_map.line_len(cursor.row()));
-            row_delta += rows.len() as u32;
 
-            new_cursors.push((selection.id, cursor.to_point(&display_map)));
+            new_cursors.push((
+                selection.id,
+                buffer.anchor_after(cursor.to_point(&display_map)),
+            ));
             edit_ranges.push(edit_start..edit_end);
         }
 
-        new_cursors.sort_unstable_by_key(|(_, point)| point.clone());
+        new_cursors.sort_unstable_by(|a, b| a.1.cmp(&b.1, &buffer).unwrap());
+        let buffer = self.buffer.update(cx, |buffer, cx| {
+            buffer.edit(edit_ranges, "", cx);
+            buffer.snapshot(cx)
+        });
         let new_selections = new_cursors
             .into_iter()
-            .map(|(id, cursor)| Selection {
-                id,
-                start: cursor,
-                end: cursor,
-                reversed: false,
-                goal: SelectionGoal::None,
+            .map(|(id, cursor)| {
+                let cursor = cursor.to_point(&buffer);
+                Selection {
+                    id,
+                    start: cursor,
+                    end: cursor,
+                    reversed: false,
+                    goal: SelectionGoal::None,
+                }
             })
             .collect();
-        self.buffer
-            .update(cx, |buffer, cx| buffer.edit(edit_ranges, "", cx));
         self.update_selections(new_selections, Some(Autoscroll::Fit), cx);
         self.end_transaction(cx);
     }
@@ -1629,7 +1680,7 @@ impl Editor {
     pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
         self.start_transaction(cx);
 
-        let mut selections = self.local_selections::<Point>(cx);
+        let selections = self.local_selections::<Point>(cx);
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
 
@@ -1659,28 +1710,13 @@ impl Editor {
             edits.push((start, text, rows.len() as u32));
         }
 
-        let mut edits_iter = edits.iter().peekable();
-        let mut row_delta = 0;
-        for selection in selections.iter_mut() {
-            while let Some((point, _, line_count)) = edits_iter.peek() {
-                if *point <= selection.start {
-                    row_delta += line_count;
-                    edits_iter.next();
-                } else {
-                    break;
-                }
-            }
-            selection.start.row += row_delta;
-            selection.end.row += row_delta;
-        }
-
         self.buffer.update(cx, |buffer, cx| {
             for (point, text, _) in edits.into_iter().rev() {
                 buffer.edit(Some(point..point), text, cx);
             }
         });
 
-        self.update_selections(selections, Some(Autoscroll::Fit), cx);
+        self.request_autoscroll(Autoscroll::Fit, cx);
         self.end_transaction(cx);
     }
 
@@ -2867,19 +2903,19 @@ impl Editor {
         loop {
             let next_group = buffer
                 .diagnostics_in_range::<_, usize>(search_start..buffer.len())
-                .find_map(|(provider_name, entry)| {
+                .find_map(|entry| {
                     if entry.diagnostic.is_primary
                         && !entry.range.is_empty()
                         && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end())
                     {
-                        Some((provider_name, entry.range, entry.diagnostic.group_id))
+                        Some((entry.range, entry.diagnostic.group_id))
                     } else {
                         None
                     }
                 });
 
-            if let Some((provider_name, primary_range, group_id)) = next_group {
-                self.activate_diagnostics(provider_name, group_id, cx);
+            if let Some((primary_range, group_id)) = next_group {
+                self.activate_diagnostics(group_id, cx);
                 self.update_selections(
                     vec![Selection {
                         id: selection.id,
@@ -2907,7 +2943,7 @@ impl Editor {
             let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
             let is_valid = buffer
                 .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone())
-                .any(|(_, entry)| {
+                .any(|entry| {
                     entry.diagnostic.is_primary
                         && !entry.range.is_empty()
                         && entry.range.start == primary_range_start
@@ -2933,12 +2969,7 @@ impl Editor {
         }
     }
 
-    fn activate_diagnostics(
-        &mut self,
-        provider_name: &str,
-        group_id: usize,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) {
         self.dismiss_diagnostics(cx);
         self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
             let buffer = self.buffer.read(cx).snapshot(cx);
@@ -2947,7 +2978,7 @@ impl Editor {
             let mut primary_message = None;
             let mut group_end = Point::zero();
             let diagnostic_group = buffer
-                .diagnostic_group::<Point>(provider_name, group_id)
+                .diagnostic_group::<Point>(group_id)
                 .map(|entry| {
                     if entry.range.end > group_end {
                         group_end = entry.range.end;
@@ -3113,10 +3144,6 @@ impl Editor {
         .collect()
     }
 
-    pub fn local_anchor_selections(&self) -> &Arc<[Selection<Anchor>]> {
-        &self.selections
-    }
-
     fn resolve_selections<'a, D, I>(
         &self,
         selections: I,
@@ -3270,6 +3297,45 @@ impl Editor {
         );
     }
 
+    /// Compute new ranges for any selections that were located in excerpts that have
+    /// since been removed.
+    ///
+    /// Returns a `HashMap` indicating which selections whose former head position
+    /// was no longer present. The keys of the map are selection ids. The values are
+    /// the id of the new excerpt where the head of the selection has been moved.
+    pub fn refresh_selections(&mut self, cx: &mut ViewContext<Self>) -> HashMap<usize, ExcerptId> {
+        let anchors_with_status = self.buffer.update(cx, |buffer, cx| {
+            let snapshot = buffer.read(cx);
+            snapshot.refresh_anchors(
+                self.selections
+                    .iter()
+                    .flat_map(|selection| [&selection.start, &selection.end]),
+            )
+        });
+        let mut selections_with_lost_position = HashMap::default();
+        self.selections = self
+            .selections
+            .iter()
+            .cloned()
+            .zip(anchors_with_status.chunks(2))
+            .map(|(mut selection, anchors)| {
+                selection.start = anchors[0].0.clone();
+                selection.end = anchors[1].0.clone();
+                let kept_head_position = if selection.reversed {
+                    anchors[0].1
+                } else {
+                    anchors[1].1
+                };
+                if !kept_head_position {
+                    selections_with_lost_position
+                        .insert(selection.id, selection.head().excerpt_id.clone());
+                }
+                selection
+            })
+            .collect();
+        selections_with_lost_position
+    }
+
     fn set_selections(&mut self, selections: Arc<[Selection<Anchor>]>, cx: &mut ViewContext<Self>) {
         self.selections = selections;
         self.buffer.update(cx, |buffer, cx| {
@@ -3650,10 +3716,14 @@ impl EditorSettings {
 fn compute_scroll_position(
     snapshot: &DisplaySnapshot,
     mut scroll_position: Vector2F,
-    scroll_top_anchor: &Anchor,
+    scroll_top_anchor: &Option<Anchor>,
 ) -> Vector2F {
-    let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32;
-    scroll_position.set_y(scroll_top + scroll_position.y());
+    if let Some(anchor) = scroll_top_anchor {
+        let scroll_top = anchor.to_display_point(snapshot).row() as f32;
+        scroll_position.set_y(scroll_top + scroll_position.y());
+    } else {
+        scroll_position.set_y(0.);
+    }
     scroll_position
 }
 
@@ -3786,6 +3856,7 @@ pub fn diagnostic_block_renderer(
         let mut text_style = settings.style.text.clone();
         text_style.color = diagnostic_style(diagnostic.severity, is_valid, &settings.style).text;
         Text::new(diagnostic.message.clone(), text_style)
+            .with_soft_wrap(false)
             .contained()
             .with_margin_left(cx.anchor_x)
             .boxed()
@@ -3801,7 +3872,8 @@ pub fn diagnostic_header_renderer(
     Arc::new(move |cx| {
         let settings = build_settings(cx);
         let mut text_style = settings.style.text.clone();
-        text_style.color = diagnostic_style(diagnostic.severity, is_valid, &settings.style).text;
+        let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
+        text_style.color = diagnostic_style.text;
         let file_path = if let Some(file) = buffer.read(&**cx).file() {
             file.path().to_string_lossy().to_string()
         } else {
@@ -3809,8 +3881,17 @@ pub fn diagnostic_header_renderer(
         };
 
         Flex::column()
-            .with_child(Label::new(diagnostic.message.clone(), text_style).boxed())
+            .with_child(
+                Text::new(diagnostic.message.clone(), text_style)
+                    .with_soft_wrap(false)
+                    .boxed(),
+            )
             .with_child(Label::new(file_path, settings.style.text.clone()).boxed())
+            .aligned()
+            .left()
+            .contained()
+            .with_style(diagnostic_style.header)
+            .expanded()
             .boxed()
     })
 }
@@ -6160,45 +6241,6 @@ mod tests {
         });
     }
 
-    impl Editor {
-        fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
-            &self,
-            cx: &mut MutableAppContext,
-        ) -> Vec<Range<D>> {
-            self.local_selections::<D>(cx)
-                .iter()
-                .map(|s| {
-                    if s.reversed {
-                        s.end.clone()..s.start.clone()
-                    } else {
-                        s.start.clone()..s.end.clone()
-                    }
-                })
-                .collect()
-        }
-
-        fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
-            let display_map = self
-                .display_map
-                .update(cx, |display_map, cx| display_map.snapshot(cx));
-            self.selections
-                .iter()
-                .chain(
-                    self.pending_selection
-                        .as_ref()
-                        .map(|pending| &pending.selection),
-                )
-                .map(|s| {
-                    if s.reversed {
-                        s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
-                    } else {
-                        s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
-                    }
-                })
-                .collect()
-        }
-    }
-
     fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
         let point = DisplayPoint::new(row as u32, column as u32);
         point..point

crates/editor/src/items.rs 🔗

@@ -5,25 +5,26 @@ use gpui::{
     elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
     Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
 };
-use language::{Diagnostic, File as _};
+use language::{Buffer, Diagnostic, File as _};
 use postage::watch;
 use project::{File, ProjectPath, Worktree};
 use std::fmt::Write;
 use std::path::Path;
 use text::{Point, Selection};
 use workspace::{
-    EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, WeakItemHandle,
+    ItemHandle, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView, WeakItemHandle,
+    Workspace,
 };
 
 pub struct BufferOpener;
 
 #[derive(Clone)]
-pub struct BufferItemHandle(pub ModelHandle<MultiBuffer>);
+pub struct BufferItemHandle(pub ModelHandle<Buffer>);
 
 #[derive(Clone)]
-struct WeakBufferItemHandle(WeakModelHandle<MultiBuffer>);
+struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
 
-impl EntryOpener for BufferOpener {
+impl PathOpener for BufferOpener {
     fn open(
         &self,
         worktree: &mut Worktree,
@@ -31,9 +32,8 @@ impl EntryOpener for BufferOpener {
         cx: &mut ModelContext<Worktree>,
     ) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
         let buffer = worktree.open_buffer(project_path.path, cx);
-        let task = cx.spawn(|_, mut cx| async move {
+        let task = cx.spawn(|_, _| async move {
             let buffer = buffer.await?;
-            let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
             Ok(Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
         });
         Some(task)
@@ -44,14 +44,15 @@ impl ItemHandle for BufferItemHandle {
     fn add_view(
         &self,
         window_id: usize,
-        settings: watch::Receiver<Settings>,
+        workspace: &Workspace,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        let buffer = self.0.downgrade();
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx));
+        let weak_buffer = buffer.downgrade();
         Box::new(cx.add_view(window_id, |cx| {
             Editor::for_buffer(
-                self.0.clone(),
-                crate::settings_builder(buffer, settings),
+                buffer,
+                crate::settings_builder(weak_buffer, workspace.settings()),
                 cx,
             )
         }))
@@ -61,16 +62,24 @@ impl ItemHandle for BufferItemHandle {
         Box::new(self.clone())
     }
 
+    fn to_any(&self) -> gpui::AnyModelHandle {
+        self.0.clone().into()
+    }
+
     fn downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
         Box::new(WeakBufferItemHandle(self.0.downgrade()))
     }
 
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
-        File::from_dyn(self.0.read(cx).file(cx)).map(|f| ProjectPath {
+        File::from_dyn(self.0.read(cx).file()).map(|f| ProjectPath {
             worktree_id: f.worktree_id(cx),
             path: f.path().clone(),
         })
     }
+
+    fn id(&self) -> usize {
+        self.0.id()
+    }
 }
 
 impl WeakItemHandle for WeakBufferItemHandle {
@@ -79,22 +88,17 @@ impl WeakItemHandle for WeakBufferItemHandle {
             .upgrade(cx)
             .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
     }
+
+    fn id(&self) -> usize {
+        self.0.id()
+    }
 }
 
 impl ItemView for Editor {
-    fn should_activate_item_on_event(event: &Event) -> bool {
-        matches!(event, Event::Activate)
-    }
+    type ItemHandle = BufferItemHandle;
 
-    fn should_close_item_on_event(event: &Event) -> bool {
-        matches!(event, Event::Closed)
-    }
-
-    fn should_update_tab_on_event(event: &Event) -> bool {
-        matches!(
-            event,
-            Event::Saved | Event::Dirtied | Event::FileHandleChanged
-        )
+    fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle {
+        BufferItemHandle(self.buffer.read(cx).as_singleton().unwrap())
     }
 
     fn title(&self, cx: &AppContext) -> String {
@@ -124,6 +128,18 @@ impl ItemView for Editor {
         Some(self.clone(cx))
     }
 
+    fn is_dirty(&self, cx: &AppContext) -> bool {
+        self.buffer().read(cx).read(cx).is_dirty()
+    }
+
+    fn has_conflict(&self, cx: &AppContext) -> bool {
+        self.buffer().read(cx).read(cx).has_conflict()
+    }
+
+    fn can_save(&self, cx: &AppContext) -> bool {
+        self.project_path(cx).is_some()
+    }
+
     fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
         let save = self.buffer().update(cx, |b, cx| b.save(cx))?;
         Ok(cx.spawn(|_, _| async move {
@@ -132,6 +148,10 @@ impl ItemView for Editor {
         }))
     }
 
+    fn can_save_as(&self, _: &AppContext) -> bool {
+        true
+    }
+
     fn save_as(
         &mut self,
         worktree: ModelHandle<Worktree>,
@@ -180,20 +200,19 @@ impl ItemView for Editor {
         })
     }
 
-    fn is_dirty(&self, cx: &AppContext) -> bool {
-        self.buffer().read(cx).read(cx).is_dirty()
-    }
-
-    fn has_conflict(&self, cx: &AppContext) -> bool {
-        self.buffer().read(cx).read(cx).has_conflict()
+    fn should_activate_item_on_event(event: &Event) -> bool {
+        matches!(event, Event::Activate)
     }
 
-    fn can_save(&self, cx: &AppContext) -> bool {
-        self.project_path(cx).is_some()
+    fn should_close_item_on_event(event: &Event) -> bool {
+        matches!(event, Event::Closed)
     }
 
-    fn can_save_as(&self, _: &AppContext) -> bool {
-        true
+    fn should_update_tab_on_event(event: &Event) -> bool {
+        matches!(
+            event,
+            Event::Saved | Event::Dirtied | Event::FileHandleChanged
+        )
     }
 }
 
@@ -298,9 +317,9 @@ impl DiagnosticMessage {
         let new_diagnostic = buffer
             .read(cx)
             .diagnostics_in_range::<_, usize>(cursor_position..cursor_position)
-            .filter(|(_, entry)| !entry.range.is_empty())
-            .min_by_key(|(_, entry)| (entry.diagnostic.severity, entry.range.len()))
-            .map(|(_, entry)| entry.diagnostic);
+            .filter(|entry| !entry.range.is_empty())
+            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
+            .map(|entry| entry.diagnostic);
         if new_diagnostic != self.diagnostic {
             self.diagnostic = new_diagnostic;
             cx.notify();

crates/editor/src/movement.rs 🔗

@@ -172,7 +172,7 @@ pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> Dis
         }
         prev_char_kind = Some(char_kind);
     }
-    point
+    map.clip_point(point, Bias::Right)
 }
 
 pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {

crates/editor/src/multi_buffer.rs 🔗

@@ -496,6 +496,14 @@ impl MultiBuffer {
             }
         }
 
+        for (buffer_id, buffer_state) in self.buffers.borrow().iter() {
+            if !selections_by_buffer.contains_key(buffer_id) {
+                buffer_state
+                    .buffer
+                    .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
+            }
+        }
+
         for (buffer_id, mut selections) in selections_by_buffer {
             self.buffers.borrow()[&buffer_id]
                 .buffer
@@ -681,6 +689,38 @@ impl MultiBuffer {
             .map_or(Vec::new(), |state| state.excerpts.clone())
     }
 
+    pub fn excerpted_buffers<'a, T: ToOffset>(
+        &'a self,
+        range: Range<T>,
+        cx: &AppContext,
+    ) -> Vec<(ModelHandle<Buffer>, Range<usize>)> {
+        let snapshot = self.snapshot(cx);
+        let start = range.start.to_offset(&snapshot);
+        let end = range.end.to_offset(&snapshot);
+
+        let mut result = Vec::new();
+        let mut cursor = snapshot.excerpts.cursor::<usize>();
+        cursor.seek(&start, Bias::Right, &());
+        while let Some(excerpt) = cursor.item() {
+            if *cursor.start() > end {
+                break;
+            }
+
+            let mut end_before_newline = cursor.end(&());
+            if excerpt.has_trailing_newline {
+                end_before_newline -= 1;
+            }
+            let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start());
+            let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start());
+            let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
+            result.push((buffer, start..end));
+            cursor.next(&());
+        }
+
+        result
+    }
+
     pub fn remove_excerpts<'a>(
         &mut self,
         excerpt_ids: impl IntoIterator<Item = &'a ExcerptId>,
@@ -1291,7 +1331,7 @@ impl MultiBufferSnapshot {
 
         let mut position = D::from_text_summary(&cursor.start().text);
         if let Some(excerpt) = cursor.item() {
-            if excerpt.id == anchor.excerpt_id {
+            if excerpt.id == anchor.excerpt_id && excerpt.buffer_id == anchor.buffer_id {
                 let excerpt_buffer_start = excerpt.range.start.summary::<D>(&excerpt.buffer);
                 let buffer_position = anchor.text_anchor.summary::<D>(&excerpt.buffer);
                 if buffer_position > excerpt_buffer_start {
@@ -1302,6 +1342,87 @@ impl MultiBufferSnapshot {
         position
     }
 
+    pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(Anchor, bool)>
+    where
+        I: 'a + IntoIterator<Item = &'a Anchor>,
+    {
+        let mut anchors = anchors.into_iter().peekable();
+        let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
+        let mut result = Vec::new();
+        while let Some(anchor) = anchors.peek() {
+            let old_excerpt_id = &anchor.excerpt_id;
+
+            // Find the location where this anchor's excerpt should be.
+            cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &());
+            if cursor.item().is_none() {
+                cursor.next(&());
+            }
+
+            let next_excerpt = cursor.item();
+            let prev_excerpt = cursor.prev_item();
+
+            // Process all of the anchors for this excerpt.
+            while let Some(&anchor) = anchors.peek() {
+                if anchor.excerpt_id != *old_excerpt_id {
+                    break;
+                }
+                let mut kept_position = false;
+                let mut anchor = anchors.next().unwrap().clone();
+
+                // Leave min and max anchors unchanged.
+                if *old_excerpt_id == ExcerptId::max() || *old_excerpt_id == ExcerptId::min() {
+                    kept_position = true;
+                }
+                // If the old excerpt still exists at this location, then leave
+                // the anchor unchanged.
+                else if next_excerpt.map_or(false, |excerpt| {
+                    excerpt.id == *old_excerpt_id && excerpt.buffer_id == anchor.buffer_id
+                }) {
+                    kept_position = true;
+                }
+                // If the old excerpt no longer exists at this location, then attempt to
+                // find an equivalent position for this anchor in an adjacent excerpt.
+                else {
+                    for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) {
+                        if excerpt.contains(&anchor) {
+                            anchor.excerpt_id = excerpt.id.clone();
+                            kept_position = true;
+                            break;
+                        }
+                    }
+                }
+                // If there's no adjacent excerpt that contains the anchor's position,
+                // then report that the anchor has lost its position.
+                if !kept_position {
+                    anchor = if let Some(excerpt) = next_excerpt {
+                        Anchor {
+                            buffer_id: excerpt.buffer_id,
+                            excerpt_id: excerpt.id.clone(),
+                            text_anchor: excerpt
+                                .buffer
+                                .anchor_at(&excerpt.range.start, anchor.text_anchor.bias),
+                        }
+                    } else if let Some(excerpt) = prev_excerpt {
+                        Anchor {
+                            buffer_id: excerpt.buffer_id,
+                            excerpt_id: excerpt.id.clone(),
+                            text_anchor: excerpt
+                                .buffer
+                                .anchor_at(&excerpt.range.end, anchor.text_anchor.bias),
+                        }
+                    } else if anchor.text_anchor.bias == Bias::Left {
+                        Anchor::min()
+                    } else {
+                        Anchor::max()
+                    };
+                }
+
+                result.push((anchor, kept_position));
+            }
+        }
+        result
+    }
+
     pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec<D>
     where
         D: TextDimension + Ord + Sub<D, Output = D>,
@@ -1312,6 +1433,7 @@ impl MultiBufferSnapshot {
         let mut summaries = Vec::new();
         while let Some(anchor) = anchors.peek() {
             let excerpt_id = &anchor.excerpt_id;
+            let buffer_id = anchor.buffer_id;
             let excerpt_anchors = iter::from_fn(|| {
                 let anchor = anchors.peek()?;
                 if anchor.excerpt_id == *excerpt_id {
@@ -1328,7 +1450,7 @@ impl MultiBufferSnapshot {
 
             let position = D::from_text_summary(&cursor.start().text);
             if let Some(excerpt) = cursor.item() {
-                if excerpt.id == *excerpt_id {
+                if excerpt.id == *excerpt_id && excerpt.buffer_id == buffer_id {
                     let excerpt_buffer_start = excerpt.range.start.summary::<D>(&excerpt.buffer);
                     summaries.extend(
                         excerpt
@@ -1379,6 +1501,7 @@ impl MultiBufferSnapshot {
             let text_anchor =
                 excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias));
             Anchor {
+                buffer_id: excerpt.buffer_id,
                 excerpt_id: excerpt.id.clone(),
                 text_anchor,
             }
@@ -1397,6 +1520,7 @@ impl MultiBufferSnapshot {
                 let text_anchor = excerpt.clip_anchor(text_anchor);
                 drop(cursor);
                 return Anchor {
+                    buffer_id: excerpt.buffer_id,
                     excerpt_id,
                     text_anchor,
                 };
@@ -1497,7 +1621,6 @@ impl MultiBufferSnapshot {
 
     pub fn diagnostic_group<'a, O>(
         &'a self,
-        provider_name: &'a str,
         group_id: usize,
     ) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
     where
@@ -1505,13 +1628,13 @@ impl MultiBufferSnapshot {
     {
         self.as_singleton()
             .into_iter()
-            .flat_map(move |buffer| buffer.diagnostic_group(provider_name, group_id))
+            .flat_map(move |buffer| buffer.diagnostic_group(group_id))
     }
 
     pub fn diagnostics_in_range<'a, T, O>(
         &'a self,
         range: Range<T>,
-    ) -> impl Iterator<Item = (&'a str, DiagnosticEntry<O>)> + 'a
+    ) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
     where
         T: 'a + ToOffset,
         O: 'a + text::FromAnchor,
@@ -1596,10 +1719,12 @@ impl MultiBufferSnapshot {
                     .flat_map(move |(replica_id, selections)| {
                         selections.map(move |selection| {
                             let mut start = Anchor {
+                                buffer_id: excerpt.buffer_id,
                                 excerpt_id: excerpt.id.clone(),
                                 text_anchor: selection.start.clone(),
                             };
                             let mut end = Anchor {
+                                buffer_id: excerpt.buffer_id,
                                 excerpt_id: excerpt.id.clone(),
                                 text_anchor: selection.end.clone(),
                             };
@@ -1795,6 +1920,22 @@ impl Excerpt {
             text_anchor
         }
     }
+
+    fn contains(&self, anchor: &Anchor) -> bool {
+        self.buffer_id == anchor.buffer_id
+            && self
+                .range
+                .start
+                .cmp(&anchor.text_anchor, &self.buffer)
+                .unwrap()
+                .is_le()
+            && self
+                .range
+                .end
+                .cmp(&anchor.text_anchor, &self.buffer)
+                .unwrap()
+                .is_ge()
+    }
 }
 
 impl fmt::Debug for Excerpt {
@@ -2370,6 +2511,131 @@ mod tests {
         assert_eq!(old_snapshot.anchor_after(10).to_offset(&new_snapshot), 14);
     }
 
+    #[gpui::test]
+    fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts(
+        cx: &mut MutableAppContext,
+    ) {
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx));
+        let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+
+        // Create an insertion id in buffer 1 that doesn't exist in buffer 2.
+        // Add an excerpt from buffer 1 that spans this new insertion.
+        buffer_1.update(cx, |buffer, cx| buffer.edit([4..4], "123", cx));
+        let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| {
+            multibuffer.push_excerpt(
+                ExcerptProperties {
+                    buffer: &buffer_1,
+                    range: 0..7,
+                },
+                cx,
+            )
+        });
+
+        let snapshot_1 = multibuffer.read(cx).snapshot(cx);
+        assert_eq!(snapshot_1.text(), "abcd123");
+
+        // Replace the buffer 1 excerpt with new excerpts from buffer 2.
+        let (excerpt_id_2, excerpt_id_3, _) = multibuffer.update(cx, |multibuffer, cx| {
+            multibuffer.remove_excerpts([&excerpt_id_1], cx);
+            (
+                multibuffer.push_excerpt(
+                    ExcerptProperties {
+                        buffer: &buffer_2,
+                        range: 0..4,
+                    },
+                    cx,
+                ),
+                multibuffer.push_excerpt(
+                    ExcerptProperties {
+                        buffer: &buffer_2,
+                        range: 6..10,
+                    },
+                    cx,
+                ),
+                multibuffer.push_excerpt(
+                    ExcerptProperties {
+                        buffer: &buffer_2,
+                        range: 12..16,
+                    },
+                    cx,
+                ),
+            )
+        });
+        let snapshot_2 = multibuffer.read(cx).snapshot(cx);
+        assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP");
+
+        // The old excerpt id has been reused.
+        assert_eq!(excerpt_id_2, excerpt_id_1);
+
+        // Resolve some anchors from the previous snapshot in the new snapshot.
+        // Although there is still an excerpt with the same id, it is for
+        // a different buffer, so we don't attempt to resolve the old text
+        // anchor in the new buffer.
+        assert_eq!(
+            snapshot_2.summary_for_anchor::<usize>(&snapshot_1.anchor_before(2)),
+            0
+        );
+        assert_eq!(
+            snapshot_2.summaries_for_anchors::<usize, _>(&[
+                snapshot_1.anchor_before(2),
+                snapshot_1.anchor_after(3)
+            ]),
+            vec![0, 0]
+        );
+        let refresh =
+            snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]);
+        assert_eq!(
+            refresh,
+            &[
+                (snapshot_2.anchor_before(0), false),
+                (snapshot_2.anchor_after(0), false),
+            ]
+        );
+
+        // Replace the middle excerpt with a smaller excerpt in buffer 2,
+        // that intersects the old excerpt.
+        let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
+            multibuffer.remove_excerpts([&excerpt_id_3], cx);
+            multibuffer.insert_excerpt_after(
+                &excerpt_id_3,
+                ExcerptProperties {
+                    buffer: &buffer_2,
+                    range: 5..8,
+                },
+                cx,
+            )
+        });
+
+        let snapshot_3 = multibuffer.read(cx).snapshot(cx);
+        assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP");
+        assert_ne!(excerpt_id_5, excerpt_id_3);
+
+        // Resolve some anchors from the previous snapshot in the new snapshot.
+        // The anchor in the middle excerpt snaps to the beginning of the
+        // excerpt, since it is not
+        let anchors = [
+            snapshot_2.anchor_before(0),
+            snapshot_2.anchor_after(2),
+            snapshot_2.anchor_after(6),
+            snapshot_2.anchor_after(14),
+        ];
+        assert_eq!(
+            snapshot_3.summaries_for_anchors::<usize, _>(&anchors),
+            &[0, 2, 9, 13]
+        );
+
+        let new_anchors = snapshot_3.refresh_anchors(&anchors);
+        assert_eq!(
+            new_anchors.iter().map(|a| a.1).collect::<Vec<_>>(),
+            &[true, true, true, true]
+        );
+        assert_eq!(
+            snapshot_3.summaries_for_anchors::<usize, _>(new_anchors.iter().map(|a| &a.0)),
+            &[0, 2, 7, 13]
+        );
+    }
+
     #[gpui::test(iterations = 100)]
     fn test_random_excerpts(cx: &mut MutableAppContext, mut rng: StdRng) {
         let operations = env::var("OPERATIONS")
@@ -2377,7 +2643,7 @@ mod tests {
             .unwrap_or(10);
 
         let mut buffers: Vec<ModelHandle<Buffer>> = Vec::new();
-        let list = cx.add_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
         let mut excerpt_ids = Vec::new();
         let mut expected_excerpts = Vec::<(ModelHandle<Buffer>, Range<text::Anchor>)>::new();
         let mut old_versions = Vec::new();
@@ -2408,7 +2674,9 @@ mod tests {
                         );
                     }
                     ids_to_remove.sort_unstable();
-                    list.update(cx, |list, cx| list.remove_excerpts(&ids_to_remove, cx));
+                    multibuffer.update(cx, |multibuffer, cx| {
+                        multibuffer.remove_excerpts(&ids_to_remove, cx)
+                    });
                 }
                 _ => {
                     let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
@@ -2440,8 +2708,8 @@ mod tests {
                         &buffer.text()[start_ix..end_ix]
                     );
 
-                    let excerpt_id = list.update(cx, |list, cx| {
-                        list.insert_excerpt_after(
+                    let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
+                        multibuffer.insert_excerpt_after(
                             &prev_excerpt_id,
                             ExcerptProperties {
                                 buffer: &buffer_handle,
@@ -2457,12 +2725,12 @@ mod tests {
             }
 
             if rng.gen_bool(0.3) {
-                list.update(cx, |list, cx| {
-                    old_versions.push((list.snapshot(cx), list.subscribe()));
+                multibuffer.update(cx, |multibuffer, cx| {
+                    old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe()));
                 })
             }
 
-            let snapshot = list.read(cx).snapshot(cx);
+            let snapshot = multibuffer.read(cx).snapshot(cx);
 
             let mut excerpt_starts = Vec::new();
             let mut expected_text = String::new();
@@ -2657,15 +2925,30 @@ mod tests {
                 let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right);
                 let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
 
+                let text_for_range = snapshot
+                    .text_for_range(start_ix..end_ix)
+                    .collect::<String>();
                 assert_eq!(
-                    snapshot
-                        .text_for_range(start_ix..end_ix)
-                        .collect::<String>(),
+                    text_for_range,
                     &expected_text[start_ix..end_ix],
                     "incorrect text for range {:?}",
                     start_ix..end_ix
                 );
 
+                let excerpted_buffer_ranges =
+                    multibuffer.read(cx).excerpted_buffers(start_ix..end_ix, cx);
+                let excerpted_buffers_text = excerpted_buffer_ranges
+                    .into_iter()
+                    .map(|(buffer, buffer_range)| {
+                        buffer
+                            .read(cx)
+                            .text_for_range(buffer_range)
+                            .collect::<String>()
+                    })
+                    .collect::<Vec<_>>()
+                    .join("\n");
+                assert_eq!(excerpted_buffers_text, text_for_range);
+
                 let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]);
                 assert_eq!(
                     snapshot.text_summary_for_range::<TextSummary, _>(start_ix..end_ix),
@@ -2699,7 +2982,7 @@ mod tests {
             }
         }
 
-        let snapshot = list.read(cx).snapshot(cx);
+        let snapshot = multibuffer.read(cx).snapshot(cx);
         for (old_snapshot, subscription) in old_versions {
             let edits = subscription.consume().into_inner();
 

crates/editor/src/multi_buffer/anchor.rs 🔗

@@ -9,6 +9,7 @@ use text::{rope::TextDimension, Point};
 
 #[derive(Clone, Eq, PartialEq, Debug, Hash)]
 pub struct Anchor {
+    pub(crate) buffer_id: usize,
     pub(crate) excerpt_id: ExcerptId,
     pub(crate) text_anchor: text::Anchor,
 }
@@ -16,6 +17,7 @@ pub struct Anchor {
 impl Anchor {
     pub fn min() -> Self {
         Self {
+            buffer_id: 0,
             excerpt_id: ExcerptId::min(),
             text_anchor: text::Anchor::min(),
         }
@@ -23,6 +25,7 @@ impl Anchor {
 
     pub fn max() -> Self {
         Self {
+            buffer_id: 0,
             excerpt_id: ExcerptId::max(),
             text_anchor: text::Anchor::max(),
         }
@@ -54,6 +57,7 @@ impl Anchor {
         if self.text_anchor.bias != Bias::Left {
             if let Some(buffer_snapshot) = snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) {
                 return Self {
+                    buffer_id: self.buffer_id,
                     excerpt_id: self.excerpt_id.clone(),
                     text_anchor: self.text_anchor.bias_left(buffer_snapshot),
                 };
@@ -66,6 +70,7 @@ impl Anchor {
         if self.text_anchor.bias != Bias::Right {
             if let Some(buffer_snapshot) = snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) {
                 return Self {
+                    buffer_id: self.buffer_id,
                     excerpt_id: self.excerpt_id.clone(),
                     text_anchor: self.text_anchor.bias_right(buffer_snapshot),
                 };

crates/editor/src/test.rs 🔗

@@ -1,33 +1,6 @@
-use gpui::{Entity, ModelHandle};
-use smol::channel;
-use std::marker::PhantomData;
-
 #[cfg(test)]
 #[ctor::ctor]
 fn init_logger() {
     // std::env::set_var("RUST_LOG", "info");
     env_logger::init();
 }
-
-pub struct Observer<T>(PhantomData<T>);
-
-impl<T: 'static> Entity for Observer<T> {
-    type Event = ();
-}
-
-impl<T: Entity> Observer<T> {
-    pub fn new(
-        handle: &ModelHandle<T>,
-        cx: &mut gpui::TestAppContext,
-    ) -> (ModelHandle<Self>, channel::Receiver<()>) {
-        let (notify_tx, notify_rx) = channel::unbounded();
-        let observer = cx.add_model(|cx| {
-            cx.observe(handle, move |_, _, _| {
-                let _ = notify_tx.try_send(());
-            })
-            .detach();
-            Observer(PhantomData)
-        });
-        (observer, notify_rx)
-    }
-}

crates/file_finder/src/file_finder.rs 🔗

@@ -83,7 +83,7 @@ impl View for FileFinder {
                                 .with_style(settings.theme.selector.input_editor.container)
                                 .boxed(),
                         )
-                        .with_child(Flexible::new(1.0, self.render_matches()).boxed())
+                        .with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
                         .boxed(),
                 )
                 .with_style(settings.theme.selector.container)
@@ -175,6 +175,7 @@ impl FileFinder {
                 .with_child(
                     Flexible::new(
                         1.0,
+                        false,
                         Flex::column()
                             .with_child(
                                 Label::new(file_name.to_string(), style.label.clone())
@@ -249,8 +250,8 @@ impl FileFinder {
         match event {
             Event::Selected(project_path) => {
                 workspace
-                    .open_entry(project_path.clone(), cx)
-                    .map(|d| d.detach());
+                    .open_path(project_path.clone(), cx)
+                    .detach_and_log_err(cx);
                 workspace.dismiss_modal(cx);
             }
             Event::Dismissed => {
@@ -430,14 +431,14 @@ mod tests {
 
     #[gpui::test]
     async fn test_matching_paths(mut cx: gpui::TestAppContext) {
-        let mut entry_openers = Vec::new();
+        let mut path_openers = Vec::new();
         cx.update(|cx| {
             super::init(cx);
-            editor::init(cx, &mut entry_openers);
+            editor::init(cx, &mut path_openers);
         });
 
         let mut params = cx.update(WorkspaceParams::test);
-        params.entry_openers = Arc::from(entry_openers);
+        params.path_openers = Arc::from(path_openers);
         params
             .fs
             .as_fake()

crates/gpui/src/app.rs 🔗

@@ -992,7 +992,7 @@ impl MutableAppContext {
         })
     }
 
-    fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+    pub fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
     where
         E: Entity,
         E::Event: 'static,
@@ -2672,9 +2672,11 @@ impl<T: Entity> ModelHandle<T> {
                         }
                     }
 
+                    cx.borrow().foreground().start_waiting();
                     rx.recv()
                         .await
                         .expect("model dropped with pending condition");
+                    cx.borrow().foreground().finish_waiting();
                 }
             })
             .await
@@ -2771,6 +2773,10 @@ impl<T: Entity> WeakModelHandle<T> {
         }
     }
 
+    pub fn id(&self) -> usize {
+        self.model_id
+    }
+
     pub fn upgrade(self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<T>> {
         cx.upgrade_model_handle(self)
     }
@@ -2914,9 +2920,11 @@ impl<T: View> ViewHandle<T> {
                         }
                     }
 
+                    cx.borrow().foreground().start_waiting();
                     rx.recv()
                         .await
                         .expect("view dropped with pending condition");
+                    cx.borrow().foreground().finish_waiting();
                 }
             })
             .await
@@ -3089,14 +3097,39 @@ impl Drop for AnyViewHandle {
 
 pub struct AnyModelHandle {
     model_id: usize,
+    model_type: TypeId,
     ref_counts: Arc<Mutex<RefCounts>>,
 }
 
+impl AnyModelHandle {
+    pub fn downcast<T: Entity>(self) -> Option<ModelHandle<T>> {
+        if self.is::<T>() {
+            let result = Some(ModelHandle {
+                model_id: self.model_id,
+                model_type: PhantomData,
+                ref_counts: self.ref_counts.clone(),
+            });
+            unsafe {
+                Arc::decrement_strong_count(&self.ref_counts);
+            }
+            std::mem::forget(self);
+            result
+        } else {
+            None
+        }
+    }
+
+    pub fn is<T: Entity>(&self) -> bool {
+        self.model_type == TypeId::of::<T>()
+    }
+}
+
 impl<T: Entity> From<ModelHandle<T>> for AnyModelHandle {
     fn from(handle: ModelHandle<T>) -> Self {
         handle.ref_counts.lock().inc_model(handle.model_id);
         Self {
             model_id: handle.model_id,
+            model_type: TypeId::of::<T>(),
             ref_counts: handle.ref_counts.clone(),
         }
     }

crates/gpui/src/elements.rs 🔗

@@ -4,6 +4,7 @@ mod constrained_box;
 mod container;
 mod empty;
 mod event_handler;
+mod expanded;
 mod flex;
 mod hook;
 mod image;
@@ -16,6 +17,7 @@ mod svg;
 mod text;
 mod uniform_list;
 
+use self::expanded::Expanded;
 pub use self::{
     align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
     hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*,
@@ -130,11 +132,18 @@ pub trait Element {
         Container::new(self.boxed())
     }
 
-    fn expanded(self, flex: f32) -> Expanded
+    fn expanded(self) -> Expanded
     where
         Self: 'static + Sized,
     {
-        Expanded::new(flex, self.boxed())
+        Expanded::new(self.boxed())
+    }
+
+    fn flexible(self, flex: f32, expanded: bool) -> Flexible
+    where
+        Self: 'static + Sized,
+    {
+        Flexible::new(flex, expanded, self.boxed())
     }
 }
 

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

@@ -0,0 +1,90 @@
+use crate::{
+    geometry::{rect::RectF, vector::Vector2F},
+    json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+    SizeConstraint,
+};
+use serde_json::json;
+
+pub struct Expanded {
+    child: ElementBox,
+    full_width: bool,
+    full_height: bool,
+}
+
+impl Expanded {
+    pub fn new(child: ElementBox) -> Self {
+        Self {
+            child,
+            full_width: true,
+            full_height: true,
+        }
+    }
+
+    pub fn to_full_width(mut self) -> Self {
+        self.full_width = true;
+        self.full_height = false;
+        self
+    }
+
+    pub fn to_full_height(mut self) -> Self {
+        self.full_width = false;
+        self.full_height = true;
+        self
+    }
+}
+
+impl Element for Expanded {
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        mut constraint: SizeConstraint,
+        cx: &mut LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        if self.full_width {
+            constraint.min.set_x(constraint.max.x());
+        }
+        if self.full_height {
+            constraint.min.set_y(constraint.max.y());
+        }
+        let size = self.child.layout(constraint, cx);
+        (size, ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        _: &mut Self::LayoutState,
+        cx: &mut PaintContext,
+    ) -> Self::PaintState {
+        self.child.paint(bounds.origin(), visible_bounds, cx);
+    }
+
+    fn dispatch_event(
+        &mut self,
+        event: &Event,
+        _: RectF,
+        _: &mut Self::LayoutState,
+        _: &mut Self::PaintState,
+        cx: &mut EventContext,
+    ) -> bool {
+        self.child.dispatch_event(event, cx)
+    }
+
+    fn debug(
+        &self,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &DebugContext,
+    ) -> json::Value {
+        json!({
+            "type": "Expanded",
+            "full_width": self.full_width,
+            "full_height": self.full_height,
+            "child": self.child.debug(cx)
+        })
+    }
+}

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

@@ -228,88 +228,15 @@ struct FlexParentData {
     expanded: bool,
 }
 
-pub struct Expanded {
-    metadata: FlexParentData,
-    child: ElementBox,
-}
-
-impl Expanded {
-    pub fn new(flex: f32, child: ElementBox) -> Self {
-        Expanded {
-            metadata: FlexParentData {
-                flex,
-                expanded: true,
-            },
-            child,
-        }
-    }
-}
-
-impl Element for Expanded {
-    type LayoutState = ();
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: SizeConstraint,
-        cx: &mut LayoutContext,
-    ) -> (Vector2F, Self::LayoutState) {
-        let size = self.child.layout(constraint, cx);
-        (size, ())
-    }
-
-    fn paint(
-        &mut self,
-        bounds: RectF,
-        visible_bounds: RectF,
-        _: &mut Self::LayoutState,
-        cx: &mut PaintContext,
-    ) -> Self::PaintState {
-        self.child.paint(bounds.origin(), visible_bounds, cx)
-    }
-
-    fn dispatch_event(
-        &mut self,
-        event: &Event,
-        _: RectF,
-        _: &mut Self::LayoutState,
-        _: &mut Self::PaintState,
-        cx: &mut EventContext,
-    ) -> bool {
-        self.child.dispatch_event(event, cx)
-    }
-
-    fn metadata(&self) -> Option<&dyn Any> {
-        Some(&self.metadata)
-    }
-
-    fn debug(
-        &self,
-        _: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        cx: &DebugContext,
-    ) -> Value {
-        json!({
-            "type": "Expanded",
-            "flex": self.metadata.flex,
-            "child": self.child.debug(cx)
-        })
-    }
-}
-
 pub struct Flexible {
     metadata: FlexParentData,
     child: ElementBox,
 }
 
 impl Flexible {
-    pub fn new(flex: f32, child: ElementBox) -> Self {
+    pub fn new(flex: f32, expanded: bool, child: ElementBox) -> Self {
         Flexible {
-            metadata: FlexParentData {
-                flex,
-                expanded: false,
-            },
+            metadata: FlexParentData { flex, expanded },
             child,
         }
     }

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

@@ -14,6 +14,7 @@ use serde_json::json;
 pub struct Text {
     text: String,
     style: TextStyle,
+    soft_wrap: bool,
 }
 
 pub struct LayoutState {
@@ -23,13 +24,22 @@ pub struct LayoutState {
 
 impl Text {
     pub fn new(text: String, style: TextStyle) -> Self {
-        Self { text, style }
+        Self {
+            text,
+            style,
+            soft_wrap: true,
+        }
     }
 
     pub fn with_default_color(mut self, color: Color) -> Self {
         self.style.color = color;
         self
     }
+
+    pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
+        self.soft_wrap = soft_wrap;
+        self
+    }
 }
 
 impl Element for Text {
@@ -54,9 +64,13 @@ impl Element for Text {
                 self.style.font_size,
                 &[(line.len(), self.style.to_run())],
             );
-            let wrap_boundaries = wrapper
-                .wrap_shaped_line(line, &shaped_line, constraint.max.x())
-                .collect::<Vec<_>>();
+            let wrap_boundaries = if self.soft_wrap {
+                wrapper
+                    .wrap_shaped_line(line, &shaped_line, constraint.max.x())
+                    .collect::<Vec<_>>()
+            } else {
+                Vec::new()
+            };
 
             max_line_width = max_line_width.max(shaped_line.width());
             line_count += wrap_boundaries.len() + 1;

crates/gpui/src/executor.rs 🔗

@@ -7,7 +7,7 @@ use rand::prelude::*;
 use smol::{channel, prelude::*, Executor, Timer};
 use std::{
     any::Any,
-    fmt::{self, Debug},
+    fmt::{self, Debug, Display},
     marker::PhantomData,
     mem,
     ops::RangeInclusive,
@@ -25,7 +25,7 @@ use waker_fn::waker_fn;
 
 use crate::{
     platform::{self, Dispatcher},
-    util,
+    util, MutableAppContext,
 };
 
 pub enum Foreground {
@@ -77,6 +77,7 @@ struct DeterministicState {
     block_on_ticks: RangeInclusive<usize>,
     now: Instant,
     pending_timers: Vec<(Instant, barrier::Sender)>,
+    waiting_backtrace: Option<Backtrace>,
 }
 
 pub struct Deterministic {
@@ -97,6 +98,7 @@ impl Deterministic {
                 block_on_ticks: 0..=1000,
                 now: Instant::now(),
                 pending_timers: Default::default(),
+                waiting_backtrace: None,
             })),
             parker: Default::default(),
         }
@@ -143,8 +145,8 @@ impl Deterministic {
                 return result;
             }
 
-            if !woken.load(SeqCst) && self.state.lock().forbid_parking {
-                panic!("deterministic executor parked after a call to forbid_parking");
+            if !woken.load(SeqCst) {
+                self.state.lock().will_park();
             }
 
             woken.store(false, SeqCst);
@@ -206,6 +208,7 @@ impl Deterministic {
                 }
 
                 let state = self.state.lock();
+
                 if state.scheduled_from_foreground.is_empty()
                     && state.scheduled_from_background.is_empty()
                     && state.spawned_from_foreground.is_empty()
@@ -244,11 +247,9 @@ impl Deterministic {
                 if let Poll::Ready(result) = future.as_mut().poll(&mut cx) {
                     return Some(result);
                 }
-                let state = self.state.lock();
+                let mut state = self.state.lock();
                 if state.scheduled_from_background.is_empty() {
-                    if state.forbid_parking {
-                        panic!("deterministic executor parked after a call to forbid_parking");
-                    }
+                    state.will_park();
                     drop(state);
                     self.parker.lock().park();
                 }
@@ -261,6 +262,26 @@ impl Deterministic {
     }
 }
 
+impl DeterministicState {
+    fn will_park(&mut self) {
+        if self.forbid_parking {
+            let mut backtrace_message = String::new();
+            if let Some(backtrace) = self.waiting_backtrace.as_mut() {
+                backtrace.resolve();
+                backtrace_message = format!(
+                    "\nbacktrace of waiting future:\n{:?}",
+                    CwdBacktrace::new(backtrace)
+                );
+            }
+
+            panic!(
+                "deterministic executor parked after a call to forbid_parking{}",
+                backtrace_message
+            );
+        }
+    }
+}
+
 #[derive(Default)]
 struct Trace {
     executed: Vec<Backtrace>,
@@ -306,32 +327,53 @@ impl Trace {
     }
 }
 
-impl Debug for Trace {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        struct FirstCwdFrameInBacktrace<'a>(&'a Backtrace);
-
-        impl<'a> Debug for FirstCwdFrameInBacktrace<'a> {
-            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
-                let cwd = std::env::current_dir().unwrap();
-                let mut print_path = |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
-                    fmt::Display::fmt(&path, fmt)
-                };
-                let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path);
-                for frame in self.0.frames() {
-                    let mut formatted_frame = fmt.frame();
-                    if frame
-                        .symbols()
-                        .iter()
-                        .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd)))
-                    {
-                        formatted_frame.backtrace_frame(frame)?;
-                        break;
-                    }
+struct CwdBacktrace<'a> {
+    backtrace: &'a Backtrace,
+    first_frame_only: bool,
+}
+
+impl<'a> CwdBacktrace<'a> {
+    fn new(backtrace: &'a Backtrace) -> Self {
+        Self {
+            backtrace,
+            first_frame_only: false,
+        }
+    }
+
+    fn first_frame(backtrace: &'a Backtrace) -> Self {
+        Self {
+            backtrace,
+            first_frame_only: true,
+        }
+    }
+}
+
+impl<'a> Debug for CwdBacktrace<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+        let cwd = std::env::current_dir().unwrap();
+        let mut print_path = |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
+            fmt::Display::fmt(&path, fmt)
+        };
+        let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path);
+        for frame in self.backtrace.frames() {
+            let mut formatted_frame = fmt.frame();
+            if frame
+                .symbols()
+                .iter()
+                .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd)))
+            {
+                formatted_frame.backtrace_frame(frame)?;
+                if self.first_frame_only {
+                    break;
                 }
-                fmt.finish()
             }
         }
+        fmt.finish()
+    }
+}
 
+impl Debug for Trace {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         for ((backtrace, scheduled), spawned_from_foreground) in self
             .executed
             .iter()
@@ -340,7 +382,7 @@ impl Debug for Trace {
         {
             writeln!(f, "Scheduled")?;
             for backtrace in scheduled {
-                writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?;
+                writeln!(f, "- {:?}", CwdBacktrace::first_frame(backtrace))?;
             }
             if scheduled.is_empty() {
                 writeln!(f, "None")?;
@@ -349,14 +391,14 @@ impl Debug for Trace {
 
             writeln!(f, "Spawned from foreground")?;
             for backtrace in spawned_from_foreground {
-                writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?;
+                writeln!(f, "- {:?}", CwdBacktrace::first_frame(backtrace))?;
             }
             if spawned_from_foreground.is_empty() {
                 writeln!(f, "None")?;
             }
             writeln!(f, "==========")?;
 
-            writeln!(f, "Run: {:?}", FirstCwdFrameInBacktrace(backtrace))?;
+            writeln!(f, "Run: {:?}", CwdBacktrace::first_frame(backtrace))?;
             writeln!(f, "+++++++++++++++++++")?;
         }
 
@@ -433,6 +475,31 @@ impl Foreground {
         *any_value.downcast().unwrap()
     }
 
+    pub fn parking_forbidden(&self) -> bool {
+        match self {
+            Self::Deterministic(executor) => executor.state.lock().forbid_parking,
+            _ => panic!("this method can only be called on a deterministic executor"),
+        }
+    }
+
+    pub fn start_waiting(&self) {
+        match self {
+            Self::Deterministic(executor) => {
+                executor.state.lock().waiting_backtrace = Some(Backtrace::new_unresolved());
+            }
+            _ => panic!("this method can only be called on a deterministic executor"),
+        }
+    }
+
+    pub fn finish_waiting(&self) {
+        match self {
+            Self::Deterministic(executor) => {
+                executor.state.lock().waiting_backtrace.take();
+            }
+            _ => panic!("this method can only be called on a deterministic executor"),
+        }
+    }
+
     pub fn forbid_parking(&self) {
         match self {
             Self::Deterministic(executor) => {
@@ -615,6 +682,17 @@ impl<T> Task<T> {
     }
 }
 
+impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
+    pub fn detach_and_log_err(self, cx: &mut MutableAppContext) {
+        cx.spawn(|_| async move {
+            if let Err(err) = self.await {
+                log::error!("{}", err);
+            }
+        })
+        .detach();
+    }
+}
+
 impl<T: Send> Task<T> {
     fn send(any_task: AnyTask) -> Self {
         Self::Send {

crates/gpui/src/test.rs 🔗

@@ -7,7 +7,13 @@ use std::{
     },
 };
 
-use crate::{executor, platform, FontCache, MutableAppContext, Platform, TestAppContext};
+use futures::StreamExt;
+use smol::channel;
+
+use crate::{
+    executor, platform, Entity, FontCache, Handle, MutableAppContext, Platform, Subscription,
+    TestAppContext,
+};
 
 #[cfg(test)]
 #[ctor::ctor]
@@ -87,3 +93,47 @@ pub fn run_test(
         }
     }
 }
+
+pub struct Observation<T> {
+    rx: channel::Receiver<T>,
+    _subscription: Subscription,
+}
+
+impl<T> futures::Stream for Observation<T> {
+    type Item = T;
+
+    fn poll_next(
+        mut self: std::pin::Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<Option<Self::Item>> {
+        self.rx.poll_next_unpin(cx)
+    }
+}
+
+pub fn observe<T: Entity>(entity: &impl Handle<T>, cx: &mut TestAppContext) -> Observation<()> {
+    let (tx, rx) = smol::channel::unbounded();
+    let _subscription = cx.update(|cx| {
+        cx.observe(entity, move |_, _| {
+            let _ = smol::block_on(tx.send(()));
+        })
+    });
+
+    Observation { rx, _subscription }
+}
+
+pub fn subscribe<T: Entity>(
+    entity: &impl Handle<T>,
+    cx: &mut TestAppContext,
+) -> Observation<T::Event>
+where
+    T::Event: Clone,
+{
+    let (tx, rx) = smol::channel::unbounded();
+    let _subscription = cx.update(|cx| {
+        cx.subscribe(entity, move |_, event, _| {
+            let _ = smol::block_on(tx.send(event.clone()));
+        })
+    });
+
+    Observation { rx, _subscription }
+}

crates/language/src/buffer.rs 🔗

@@ -65,9 +65,9 @@ pub struct Buffer {
     syntax_tree: Mutex<Option<SyntaxTree>>,
     parsing_in_background: bool,
     parse_count: usize,
+    diagnostics: DiagnosticSet,
     remote_selections: TreeMap<ReplicaId, SelectionSet>,
     selections_update_count: usize,
-    diagnostic_sets: Vec<DiagnosticSet>,
     diagnostics_update_count: usize,
     language_server: Option<LanguageServerState>,
     deferred_ops: OperationQueue<Operation>,
@@ -78,7 +78,7 @@ pub struct Buffer {
 pub struct BufferSnapshot {
     text: text::BufferSnapshot,
     tree: Option<Tree>,
-    diagnostic_sets: Vec<DiagnosticSet>,
+    diagnostics: DiagnosticSet,
     diagnostics_update_count: usize,
     remote_selections: TreeMap<ReplicaId, SelectionSet>,
     selections_update_count: usize,
@@ -129,7 +129,6 @@ struct LanguageServerSnapshot {
 pub enum Operation {
     Buffer(text::Operation),
     UpdateDiagnostics {
-        provider_name: String,
         diagnostics: Arc<[DiagnosticEntry<Anchor>]>,
         lamport_timestamp: clock::Lamport,
     },
@@ -323,17 +322,11 @@ impl Buffer {
             );
         }
         let snapshot = this.snapshot();
-        for diagnostic_set in message.diagnostic_sets {
-            let (provider_name, entries) = proto::deserialize_diagnostic_set(diagnostic_set);
-            this.apply_diagnostic_update(
-                DiagnosticSet::from_sorted_entries(
-                    provider_name,
-                    entries.into_iter().cloned(),
-                    &snapshot,
-                ),
-                cx,
-            );
-        }
+        let entries = proto::deserialize_diagnostics(message.diagnostics);
+        this.apply_diagnostic_update(
+            DiagnosticSet::from_sorted_entries(entries.into_iter().cloned(), &snapshot),
+            cx,
+        );
 
         let deferred_ops = message
             .deferred_operations
@@ -371,13 +364,7 @@ impl Buffer {
                     lamport_timestamp: set.lamport_timestamp.value,
                 })
                 .collect(),
-            diagnostic_sets: self
-                .diagnostic_sets
-                .iter()
-                .map(|set| {
-                    proto::serialize_diagnostic_set(set.provider_name().to_string(), set.iter())
-                })
-                .collect(),
+            diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
             deferred_operations: self
                 .deferred_ops
                 .iter()
@@ -423,7 +410,7 @@ impl Buffer {
             language: None,
             remote_selections: Default::default(),
             selections_update_count: 0,
-            diagnostic_sets: Default::default(),
+            diagnostics: Default::default(),
             diagnostics_update_count: 0,
             language_server: None,
             deferred_ops: OperationQueue::new(),
@@ -437,7 +424,7 @@ impl Buffer {
             text: self.text.snapshot(),
             tree: self.syntax_tree(),
             remote_selections: self.remote_selections.clone(),
-            diagnostic_sets: self.diagnostic_sets.clone(),
+            diagnostics: self.diagnostics.clone(),
             diagnostics_update_count: self.diagnostics_update_count,
             is_parsing: self.parsing_in_background,
             language: self.language.clone(),
@@ -793,7 +780,6 @@ impl Buffer {
 
     pub fn update_diagnostics<T>(
         &mut self,
-        provider_name: Arc<str>,
         version: Option<i32>,
         mut diagnostics: Vec<DiagnosticEntry<T>>,
         cx: &mut ModelContext<Self>,
@@ -883,10 +869,9 @@ impl Buffer {
         }
         drop(edits_since_save);
 
-        let set = DiagnosticSet::new(provider_name, sanitized_diagnostics, content);
+        let set = DiagnosticSet::new(sanitized_diagnostics, content);
         self.apply_diagnostic_update(set.clone(), cx);
         Ok(Operation::UpdateDiagnostics {
-            provider_name: set.provider_name().to_string(),
             diagnostics: set.iter().cloned().collect(),
             lamport_timestamp: self.text.lamport_clock.tick(),
         })
@@ -1395,17 +1380,12 @@ impl Buffer {
                 unreachable!("buffer operations should never be applied at this layer")
             }
             Operation::UpdateDiagnostics {
-                provider_name,
                 diagnostics: diagnostic_set,
                 ..
             } => {
                 let snapshot = self.snapshot();
                 self.apply_diagnostic_update(
-                    DiagnosticSet::from_sorted_entries(
-                        provider_name,
-                        diagnostic_set.iter().cloned(),
-                        &snapshot,
-                    ),
+                    DiagnosticSet::from_sorted_entries(diagnostic_set.iter().cloned(), &snapshot),
                     cx,
                 );
             }
@@ -1433,15 +1413,8 @@ impl Buffer {
         }
     }
 
-    fn apply_diagnostic_update(&mut self, set: DiagnosticSet, cx: &mut ModelContext<Self>) {
-        match self
-            .diagnostic_sets
-            .binary_search_by_key(&set.provider_name(), |set| set.provider_name())
-        {
-            Ok(ix) => self.diagnostic_sets[ix] = set.clone(),
-            Err(ix) => self.diagnostic_sets.insert(ix, set.clone()),
-        }
-
+    fn apply_diagnostic_update(&mut self, diagnostics: DiagnosticSet, cx: &mut ModelContext<Self>) {
+        self.diagnostics = diagnostics;
         self.diagnostics_update_count += 1;
         cx.notify();
         cx.emit(Event::DiagnosticsUpdated);
@@ -1712,7 +1685,7 @@ impl BufferSnapshot {
         let mut highlights = None;
         let mut diagnostic_endpoints = Vec::<DiagnosticEndpoint>::new();
         if let Some(theme) = theme {
-            for (_, entry) in self.diagnostics_in_range::<_, usize>(range.clone()) {
+            for entry in self.diagnostics_in_range::<_, usize>(range.clone()) {
                 diagnostic_endpoints.push(DiagnosticEndpoint {
                     offset: entry.range.start,
                     is_start: true,
@@ -1853,38 +1826,28 @@ impl BufferSnapshot {
     pub fn diagnostics_in_range<'a, T, O>(
         &'a self,
         search_range: Range<T>,
-    ) -> impl 'a + Iterator<Item = (&'a str, DiagnosticEntry<O>)>
+    ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
     where
         T: 'a + Clone + ToOffset,
         O: 'a + FromAnchor,
     {
-        self.diagnostic_sets.iter().flat_map(move |set| {
-            set.range(search_range.clone(), self, true)
-                .map(|e| (set.provider_name(), e))
-        })
+        self.diagnostics.range(search_range.clone(), self, true)
     }
 
     pub fn diagnostic_groups(&self) -> Vec<DiagnosticGroup<Anchor>> {
         let mut groups = Vec::new();
-        for set in &self.diagnostic_sets {
-            set.groups(&mut groups, self);
-        }
+        self.diagnostics.groups(&mut groups, self);
         groups
     }
 
     pub fn diagnostic_group<'a, O>(
         &'a self,
-        provider_name: &str,
         group_id: usize,
     ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
     where
         O: 'a + FromAnchor,
     {
-        self.diagnostic_sets
-            .iter()
-            .find(|s| s.provider_name() == provider_name)
-            .into_iter()
-            .flat_map(move |s| s.group(group_id, self))
+        self.diagnostics.group(group_id, self)
     }
 
     pub fn diagnostics_update_count(&self) -> usize {
@@ -1906,8 +1869,8 @@ impl Clone for BufferSnapshot {
             text: self.text.clone(),
             tree: self.tree.clone(),
             remote_selections: self.remote_selections.clone(),
+            diagnostics: self.diagnostics.clone(),
             selections_update_count: self.selections_update_count,
-            diagnostic_sets: self.diagnostic_sets.clone(),
             diagnostics_update_count: self.diagnostics_update_count,
             is_parsing: self.is_parsing,
             language: self.language.clone(),

crates/language/src/diagnostic_set.rs 🔗

@@ -4,14 +4,12 @@ use std::{
     cmp::{Ordering, Reverse},
     iter,
     ops::Range,
-    sync::Arc,
 };
 use sum_tree::{self, Bias, SumTree};
 use text::{Anchor, FromAnchor, Point, ToOffset};
 
 #[derive(Clone, Debug)]
 pub struct DiagnosticSet {
-    provider_name: Arc<str>,
     diagnostics: SumTree<DiagnosticEntry<Anchor>>,
 }
 
@@ -21,6 +19,7 @@ pub struct DiagnosticEntry<T> {
     pub diagnostic: Diagnostic,
 }
 
+#[derive(Debug)]
 pub struct DiagnosticGroup<T> {
     pub entries: Vec<DiagnosticEntry<T>>,
     pub primary_ix: usize,
@@ -36,32 +35,22 @@ pub struct Summary {
 }
 
 impl DiagnosticSet {
-    pub fn provider_name(&self) -> &str {
-        &self.provider_name
-    }
-
-    pub fn from_sorted_entries<I>(
-        provider_name: impl Into<Arc<str>>,
-        iter: I,
-        buffer: &text::BufferSnapshot,
-    ) -> Self
+    pub fn from_sorted_entries<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
     where
         I: IntoIterator<Item = DiagnosticEntry<Anchor>>,
     {
         Self {
-            provider_name: provider_name.into(),
             diagnostics: SumTree::from_iter(iter, buffer),
         }
     }
 
-    pub fn new<I>(provider_name: Arc<str>, iter: I, buffer: &text::BufferSnapshot) -> Self
+    pub fn new<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
     where
         I: IntoIterator<Item = DiagnosticEntry<Point>>,
     {
         let mut entries = iter.into_iter().collect::<Vec<_>>();
         entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end)));
         Self {
-            provider_name,
             diagnostics: SumTree::from_iter(
                 entries.into_iter().map(|entry| DiagnosticEntry {
                     range: buffer.anchor_before(entry.range.start)
@@ -159,7 +148,6 @@ impl DiagnosticSet {
 impl Default for DiagnosticSet {
     fn default() -> Self {
         Self {
-            provider_name: "".into(),
             diagnostics: Default::default(),
         }
     }

crates/language/src/language.rs 🔗

@@ -6,10 +6,9 @@ pub mod proto;
 mod tests;
 
 use anyhow::{anyhow, Result};
-use async_trait::async_trait;
 pub use buffer::Operation;
 pub use buffer::*;
-use collections::{HashMap, HashSet};
+use collections::HashSet;
 pub use diagnostic_set::DiagnosticEntry;
 use gpui::AppContext;
 use highlight_map::HighlightMap;
@@ -47,6 +46,7 @@ pub struct LanguageConfig {
 pub struct LanguageServerConfig {
     pub binary: String,
     pub disk_based_diagnostic_sources: HashSet<String>,
+    pub disk_based_diagnostics_progress_token: Option<String>,
     #[cfg(any(test, feature = "test-support"))]
     #[serde(skip)]
     pub fake_server: Option<(Arc<lsp::LanguageServer>, Arc<std::sync::atomic::AtomicBool>)>,
@@ -60,18 +60,9 @@ pub struct BracketPair {
     pub newline: bool,
 }
 
-#[async_trait]
-pub trait DiagnosticProvider: 'static + Send + Sync {
-    async fn diagnose(
-        &self,
-        path: Arc<Path>,
-    ) -> Result<HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>>;
-}
-
 pub struct Language {
     pub(crate) config: LanguageConfig,
     pub(crate) grammar: Option<Arc<Grammar>>,
-    pub(crate) diagnostic_provider: Option<Arc<dyn DiagnosticProvider>>,
 }
 
 pub struct Grammar {
@@ -136,7 +127,6 @@ impl Language {
                     highlight_map: Default::default(),
                 })
             }),
-            diagnostic_provider: None,
         }
     }
 
@@ -170,11 +160,6 @@ impl Language {
         Ok(self)
     }
 
-    pub fn with_diagnostic_provider(mut self, source: impl DiagnosticProvider) -> Self {
-        self.diagnostic_provider = Some(Arc::new(source));
-        self
-    }
-
     pub fn name(&self) -> &str {
         self.config.name.as_str()
     }
@@ -208,10 +193,6 @@ impl Language {
         }
     }
 
-    pub fn diagnostic_provider(&self) -> Option<&Arc<dyn DiagnosticProvider>> {
-        self.diagnostic_provider.as_ref()
-    }
-
     pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
         self.config
             .language_server
@@ -219,6 +200,13 @@ impl Language {
             .map(|config| &config.disk_based_diagnostic_sources)
     }
 
+    pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> {
+        self.config
+            .language_server
+            .as_ref()
+            .and_then(|config| config.disk_based_diagnostics_progress_token.as_ref())
+    }
+
     pub fn brackets(&self) -> &[BracketPair] {
         &self.config.brackets
     }
@@ -249,6 +237,7 @@ impl LanguageServerConfig {
         (
             Self {
                 fake_server: Some((server, started)),
+                disk_based_diagnostics_progress_token: Some("fakeServer/check".to_string()),
                 ..Default::default()
             },
             fake,

crates/language/src/proto.rs 🔗

@@ -51,16 +51,12 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
                 selections: serialize_selections(selections),
             }),
             Operation::UpdateDiagnostics {
-                provider_name,
                 diagnostics,
                 lamport_timestamp,
-            } => proto::operation::Variant::UpdateDiagnosticSet(proto::UpdateDiagnosticSet {
+            } => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics {
                 replica_id: lamport_timestamp.replica_id as u32,
                 lamport_timestamp: lamport_timestamp.value,
-                diagnostic_set: Some(serialize_diagnostic_set(
-                    provider_name.clone(),
-                    diagnostics.iter(),
-                )),
+                diagnostics: serialize_diagnostics(diagnostics.iter()),
             }),
         }),
     }
@@ -134,33 +130,29 @@ pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto:
         .collect()
 }
 
-pub fn serialize_diagnostic_set<'a>(
-    provider_name: String,
+pub fn serialize_diagnostics<'a>(
     diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<Anchor>>,
-) -> proto::DiagnosticSet {
-    proto::DiagnosticSet {
-        provider_name,
-        diagnostics: diagnostics
-            .into_iter()
-            .map(|entry| proto::Diagnostic {
-                start: Some(serialize_anchor(&entry.range.start)),
-                end: Some(serialize_anchor(&entry.range.end)),
-                message: entry.diagnostic.message.clone(),
-                severity: match entry.diagnostic.severity {
-                    DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
-                    DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
-                    DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
-                    DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
-                    _ => proto::diagnostic::Severity::None,
-                } as i32,
-                group_id: entry.diagnostic.group_id as u64,
-                is_primary: entry.diagnostic.is_primary,
-                is_valid: entry.diagnostic.is_valid,
-                code: entry.diagnostic.code.clone(),
-                is_disk_based: entry.diagnostic.is_disk_based,
-            })
-            .collect(),
-    }
+) -> Vec<proto::Diagnostic> {
+    diagnostics
+        .into_iter()
+        .map(|entry| proto::Diagnostic {
+            start: Some(serialize_anchor(&entry.range.start)),
+            end: Some(serialize_anchor(&entry.range.end)),
+            message: entry.diagnostic.message.clone(),
+            severity: match entry.diagnostic.severity {
+                DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
+                DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
+                DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
+                DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
+                _ => proto::diagnostic::Severity::None,
+            } as i32,
+            group_id: entry.diagnostic.group_id as u64,
+            is_primary: entry.diagnostic.is_primary,
+            is_valid: entry.diagnostic.is_valid,
+            code: entry.diagnostic.code.clone(),
+            is_disk_based: entry.diagnostic.is_disk_based,
+        })
+        .collect()
 }
 
 fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
@@ -239,21 +231,13 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
                     selections: Arc::from(selections),
                 }
             }
-            proto::operation::Variant::UpdateDiagnosticSet(message) => {
-                let (provider_name, diagnostics) = deserialize_diagnostic_set(
-                    message
-                        .diagnostic_set
-                        .ok_or_else(|| anyhow!("missing diagnostic set"))?,
-                );
-                Operation::UpdateDiagnostics {
-                    provider_name,
-                    diagnostics,
-                    lamport_timestamp: clock::Lamport {
-                        replica_id: message.replica_id as ReplicaId,
-                        value: message.lamport_timestamp,
-                    },
-                }
-            }
+            proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics {
+                diagnostics: deserialize_diagnostics(message.diagnostics),
+                lamport_timestamp: clock::Lamport {
+                    replica_id: message.replica_id as ReplicaId,
+                    value: message.lamport_timestamp,
+                },
+            },
         },
     )
 }
@@ -340,40 +324,32 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
     )
 }
 
-pub fn deserialize_diagnostic_set(
-    message: proto::DiagnosticSet,
-) -> (String, Arc<[DiagnosticEntry<Anchor>]>) {
-    (
-        message.provider_name,
-        message
-            .diagnostics
-            .into_iter()
-            .filter_map(|diagnostic| {
-                Some(DiagnosticEntry {
-                    range: deserialize_anchor(diagnostic.start?)?
-                        ..deserialize_anchor(diagnostic.end?)?,
-                    diagnostic: Diagnostic {
-                        severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)?
-                        {
-                            proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
-                            proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
-                            proto::diagnostic::Severity::Information => {
-                                DiagnosticSeverity::INFORMATION
-                            }
-                            proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
-                            proto::diagnostic::Severity::None => return None,
-                        },
-                        message: diagnostic.message,
-                        group_id: diagnostic.group_id as usize,
-                        code: diagnostic.code,
-                        is_valid: diagnostic.is_valid,
-                        is_primary: diagnostic.is_primary,
-                        is_disk_based: diagnostic.is_disk_based,
+pub fn deserialize_diagnostics(
+    diagnostics: Vec<proto::Diagnostic>,
+) -> Arc<[DiagnosticEntry<Anchor>]> {
+    diagnostics
+        .into_iter()
+        .filter_map(|diagnostic| {
+            Some(DiagnosticEntry {
+                range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?,
+                diagnostic: Diagnostic {
+                    severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? {
+                        proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
+                        proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
+                        proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION,
+                        proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
+                        proto::diagnostic::Severity::None => return None,
                     },
-                })
+                    message: diagnostic.message,
+                    group_id: diagnostic.group_id as usize,
+                    code: diagnostic.code,
+                    is_valid: diagnostic.is_valid,
+                    is_primary: diagnostic.is_primary,
+                    is_disk_based: diagnostic.is_disk_based,
+                },
             })
-            .collect(),
-    )
+        })
+        .collect()
 }
 
 fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {

crates/language/src/tests.rs 🔗

@@ -460,7 +460,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
         // Receive diagnostics for an earlier version of the buffer.
         buffer
             .update_diagnostics(
-                "lsp".into(),
                 Some(open_notification.text_document.version),
                 vec![
                     DiagnosticEntry {
@@ -508,34 +507,28 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
                 .collect::<Vec<_>>(),
             &[
-                (
-                    "lsp",
-                    DiagnosticEntry {
-                        range: Point::new(3, 9)..Point::new(3, 11),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::ERROR,
-                            message: "undefined variable 'BB'".to_string(),
-                            is_disk_based: true,
-                            group_id: 1,
-                            is_primary: true,
-                            ..Default::default()
-                        },
-                    }
-                ),
-                (
-                    "lsp",
-                    DiagnosticEntry {
-                        range: Point::new(4, 9)..Point::new(4, 12),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::ERROR,
-                            message: "undefined variable 'CCC'".to_string(),
-                            is_disk_based: true,
-                            group_id: 2,
-                            is_primary: true,
-                            ..Default::default()
-                        }
+                DiagnosticEntry {
+                    range: Point::new(3, 9)..Point::new(3, 11),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'BB'".to_string(),
+                        is_disk_based: true,
+                        group_id: 1,
+                        is_primary: true,
+                        ..Default::default()
+                    },
+                },
+                DiagnosticEntry {
+                    range: Point::new(4, 9)..Point::new(4, 12),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'CCC'".to_string(),
+                        is_disk_based: true,
+                        group_id: 2,
+                        is_primary: true,
+                        ..Default::default()
                     }
-                )
+                }
             ]
         );
         assert_eq!(
@@ -562,7 +555,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
         // Ensure overlapping diagnostics are highlighted correctly.
         buffer
             .update_diagnostics(
-                "lsp".into(),
                 Some(open_notification.text_document.version),
                 vec![
                     DiagnosticEntry {
@@ -596,33 +588,27 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
                 .collect::<Vec<_>>(),
             &[
-                (
-                    "lsp",
-                    DiagnosticEntry {
-                        range: Point::new(2, 9)..Point::new(2, 12),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::WARNING,
-                            message: "unreachable statement".to_string(),
-                            group_id: 1,
-                            is_primary: true,
-                            ..Default::default()
-                        }
-                    }
-                ),
-                (
-                    "lsp",
-                    DiagnosticEntry {
-                        range: Point::new(2, 9)..Point::new(2, 10),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::ERROR,
-                            message: "undefined variable 'A'".to_string(),
-                            is_disk_based: true,
-                            group_id: 0,
-                            is_primary: true,
-                            ..Default::default()
-                        },
+                DiagnosticEntry {
+                    range: Point::new(2, 9)..Point::new(2, 12),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::WARNING,
+                        message: "unreachable statement".to_string(),
+                        group_id: 1,
+                        is_primary: true,
+                        ..Default::default()
                     }
-                )
+                },
+                DiagnosticEntry {
+                    range: Point::new(2, 9)..Point::new(2, 10),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'A'".to_string(),
+                        is_disk_based: true,
+                        group_id: 0,
+                        is_primary: true,
+                        ..Default::default()
+                    },
+                }
             ]
         );
         assert_eq!(
@@ -659,7 +645,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
     buffer.update(&mut cx, |buffer, cx| {
         buffer
             .update_diagnostics(
-                "lsp".into(),
                 Some(change_notification_2.text_document.version),
                 vec![
                     DiagnosticEntry {
@@ -694,34 +679,28 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                 .diagnostics_in_range::<_, Point>(0..buffer.len())
                 .collect::<Vec<_>>(),
             &[
-                (
-                    "lsp",
-                    DiagnosticEntry {
-                        range: Point::new(2, 21)..Point::new(2, 22),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::ERROR,
-                            message: "undefined variable 'A'".to_string(),
-                            is_disk_based: true,
-                            group_id: 0,
-                            is_primary: true,
-                            ..Default::default()
-                        }
+                DiagnosticEntry {
+                    range: Point::new(2, 21)..Point::new(2, 22),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'A'".to_string(),
+                        is_disk_based: true,
+                        group_id: 0,
+                        is_primary: true,
+                        ..Default::default()
                     }
-                ),
-                (
-                    "lsp",
-                    DiagnosticEntry {
-                        range: Point::new(3, 9)..Point::new(3, 11),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::ERROR,
-                            message: "undefined variable 'BB'".to_string(),
-                            is_disk_based: true,
-                            group_id: 1,
-                            is_primary: true,
-                            ..Default::default()
-                        },
-                    }
-                )
+                },
+                DiagnosticEntry {
+                    range: Point::new(3, 9)..Point::new(3, 11),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'BB'".to_string(),
+                        is_disk_based: true,
+                        group_id: 1,
+                        is_primary: true,
+                        ..Default::default()
+                    },
+                }
             ]
         );
     });
@@ -740,7 +719,6 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
         buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
         buffer
             .update_diagnostics(
-                "lsp".into(),
                 None,
                 vec![
                     DiagnosticEntry {

crates/lsp/src/lsp.rs 🔗

@@ -28,7 +28,7 @@ pub use lsp_types::*;
 const JSON_RPC_VERSION: &'static str = "2.0";
 const CONTENT_LEN_HEADER: &'static str = "Content-Length: ";
 
-type NotificationHandler = Box<dyn Send + Sync + Fn(&str)>;
+type NotificationHandler = Box<dyn Send + Sync + FnMut(&str)>;
 type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
 
 pub struct LanguageServer {
@@ -139,7 +139,7 @@ impl LanguageServer {
                         if let Ok(AnyNotification { method, params }) =
                             serde_json::from_slice(&buffer)
                         {
-                            if let Some(handler) = notification_handlers.read().get(method) {
+                            if let Some(handler) = notification_handlers.write().get_mut(method) {
                                 handler(params.get());
                             } else {
                                 log::info!(
@@ -226,15 +226,15 @@ impl LanguageServer {
             process_id: Default::default(),
             root_path: Default::default(),
             root_uri: Some(root_uri),
-            initialization_options: Some(json!({
-                "checkOnSave": {
-                    "enable": false
-                },
-            })),
+            initialization_options: Default::default(),
             capabilities: lsp_types::ClientCapabilities {
                 experimental: Some(json!({
                     "serverStatusNotification": true,
                 })),
+                window: Some(lsp_types::WindowClientCapabilities {
+                    work_done_progress: Some(true),
+                    ..Default::default()
+                }),
                 ..Default::default()
             },
             trace: Default::default(),
@@ -283,10 +283,10 @@ impl LanguageServer {
         }
     }
 
-    pub fn on_notification<T, F>(&self, f: F) -> Subscription
+    pub fn on_notification<T, F>(&self, mut f: F) -> Subscription
     where
         T: lsp_types::notification::Notification,
-        F: 'static + Send + Sync + Fn(T::Params),
+        F: 'static + Send + Sync + FnMut(T::Params),
     {
         let prev_handler = self.notification_handlers.write().insert(
             T::METHOD,
@@ -514,6 +514,22 @@ impl FakeLanguageServer {
         notification.params
     }
 
+    pub async fn start_progress(&mut self, token: impl Into<String>) {
+        self.notify::<notification::Progress>(ProgressParams {
+            token: NumberOrString::String(token.into()),
+            value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())),
+        })
+        .await;
+    }
+
+    pub async fn end_progress(&mut self, token: impl Into<String>) {
+        self.notify::<notification::Progress>(ProgressParams {
+            token: NumberOrString::String(token.into()),
+            value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())),
+        })
+        .await;
+    }
+
     async fn send(&mut self, message: Vec<u8>) {
         self.stdout
             .write_all(CONTENT_LEN_HEADER.as_bytes())

crates/project/src/project.rs 🔗

@@ -1,6 +1,6 @@
 pub mod fs;
 mod ignore;
-mod worktree;
+pub mod worktree;
 
 use anyhow::{anyhow, Result};
 use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
@@ -18,7 +18,7 @@ use std::{
     path::Path,
     sync::{atomic::AtomicBool, Arc},
 };
-use util::{ResultExt, TryFutureExt as _};
+use util::TryFutureExt as _;
 
 pub use fs::*;
 pub use worktree::*;
@@ -33,6 +33,7 @@ pub struct Project {
     client_state: ProjectClientState,
     collaborators: HashMap<PeerId, Collaborator>,
     subscriptions: Vec<client::Subscription>,
+    pending_disk_based_diagnostics: isize,
 }
 
 enum ProjectClientState {
@@ -60,6 +61,9 @@ pub struct Collaborator {
 pub enum Event {
     ActiveEntryChanged(Option<ProjectEntry>),
     WorktreeRemoved(WorktreeId),
+    DiskBasedDiagnosticsStarted,
+    DiskBasedDiagnosticsUpdated { worktree_id: WorktreeId },
+    DiskBasedDiagnosticsFinished,
     DiagnosticsUpdated(ProjectPath),
 }
 
@@ -69,7 +73,7 @@ pub struct ProjectPath {
     pub path: Arc<Path>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug, Default, PartialEq)]
 pub struct DiagnosticSummary {
     pub error_count: usize,
     pub warning_count: usize,
@@ -100,6 +104,16 @@ impl DiagnosticSummary {
 
         this
     }
+
+    pub fn to_proto(&self, path: Arc<Path>) -> proto::DiagnosticSummary {
+        proto::DiagnosticSummary {
+            path: path.to_string_lossy().to_string(),
+            error_count: self.error_count as u32,
+            warning_count: self.warning_count as u32,
+            info_count: self.info_count as u32,
+            hint_count: self.hint_count as u32,
+        }
+    }
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -176,6 +190,7 @@ impl Project {
                 client,
                 user_store,
                 fs,
+                pending_disk_based_diagnostics: 0,
             }
         })
     }
@@ -228,29 +243,51 @@ impl Project {
             collaborators.insert(collaborator.peer_id, collaborator);
         }
 
-        Ok(cx.add_model(|cx| Self {
-            worktrees,
-            active_entry: None,
-            collaborators,
-            languages,
-            user_store,
-            fs,
-            subscriptions: vec![
-                client.subscribe_to_entity(remote_id, cx, Self::handle_unshare_project),
-                client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator),
-                client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator),
-                client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree),
-                client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree),
-                client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
-                client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
-                client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
-            ],
-            client,
-            client_state: ProjectClientState::Remote {
-                sharing_has_stopped: false,
-                remote_id,
-                replica_id,
-            },
+        Ok(cx.add_model(|cx| {
+            let mut this = Self {
+                worktrees: Vec::new(),
+                active_entry: None,
+                collaborators,
+                languages,
+                user_store,
+                fs,
+                subscriptions: vec![
+                    client.subscribe_to_entity(remote_id, cx, Self::handle_unshare_project),
+                    client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator),
+                    client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator),
+                    client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree),
+                    client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree),
+                    client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
+                    client.subscribe_to_entity(
+                        remote_id,
+                        cx,
+                        Self::handle_update_diagnostic_summary,
+                    ),
+                    client.subscribe_to_entity(
+                        remote_id,
+                        cx,
+                        Self::handle_disk_based_diagnostics_updating,
+                    ),
+                    client.subscribe_to_entity(
+                        remote_id,
+                        cx,
+                        Self::handle_disk_based_diagnostics_updated,
+                    ),
+                    client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
+                    client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
+                ],
+                client,
+                client_state: ProjectClientState::Remote {
+                    sharing_has_stopped: false,
+                    remote_id,
+                    replica_id,
+                },
+                pending_disk_based_diagnostics: 0,
+            };
+            for worktree in worktrees {
+                this.add_worktree(worktree, cx);
+            }
+            this
         }))
     }
 
@@ -479,13 +516,30 @@ impl Project {
 
     fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
         cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
-        cx.subscribe(&worktree, |_, worktree, event, cx| match event {
+        cx.subscribe(&worktree, move |this, worktree, event, cx| match event {
             worktree::Event::DiagnosticsUpdated(path) => {
                 cx.emit(Event::DiagnosticsUpdated(ProjectPath {
                     worktree_id: worktree.read(cx).id(),
                     path: path.clone(),
                 }));
             }
+            worktree::Event::DiskBasedDiagnosticsUpdating => {
+                if this.pending_disk_based_diagnostics == 0 {
+                    cx.emit(Event::DiskBasedDiagnosticsStarted);
+                }
+                this.pending_disk_based_diagnostics += 1;
+            }
+            worktree::Event::DiskBasedDiagnosticsUpdated => {
+                this.pending_disk_based_diagnostics -= 1;
+                cx.emit(Event::DiskBasedDiagnosticsUpdated {
+                    worktree_id: worktree.read(cx).id(),
+                });
+                if this.pending_disk_based_diagnostics == 0 {
+                    if this.pending_disk_based_diagnostics == 0 {
+                        cx.emit(Event::DiskBasedDiagnosticsFinished);
+                    }
+                }
+            }
         })
         .detach();
         self.worktrees.push(worktree);
@@ -507,34 +561,19 @@ impl Project {
         }
     }
 
-    pub fn diagnose(&self, cx: &mut ModelContext<Self>) {
-        for worktree_handle in &self.worktrees {
-            if let Some(worktree) = worktree_handle.read(cx).as_local() {
-                for language in worktree.languages() {
-                    if let Some(provider) = language.diagnostic_provider().cloned() {
-                        let worktree_path = worktree.abs_path().clone();
-                        let worktree_handle = worktree_handle.downgrade();
-                        cx.spawn_weak(|_, mut cx| async move {
-                            let diagnostics = provider.diagnose(worktree_path).await.log_err()?;
-                            let worktree_handle = worktree_handle.upgrade(&cx)?;
-                            worktree_handle.update(&mut cx, |worktree, cx| {
-                                for (path, diagnostics) in diagnostics {
-                                    worktree
-                                        .update_diagnostics_from_provider(
-                                            path.into(),
-                                            diagnostics,
-                                            cx,
-                                        )
-                                        .log_err()?;
-                                }
-                                Some(())
-                            })
-                        })
-                        .detach();
-                    }
-                }
-            }
+    pub fn is_running_disk_based_diagnostics(&self) -> bool {
+        self.pending_disk_based_diagnostics > 0
+    }
+
+    pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
+        let mut summary = DiagnosticSummary::default();
+        for (_, path_summary) in self.diagnostic_summaries(cx) {
+            summary.error_count += path_summary.error_count;
+            summary.warning_count += path_summary.warning_count;
+            summary.info_count += path_summary.info_count;
+            summary.hint_count += path_summary.hint_count;
         }
+        summary
     }
 
     pub fn diagnostic_summaries<'a>(
@@ -685,6 +724,60 @@ impl Project {
         Ok(())
     }
 
+    fn handle_update_diagnostic_summary(
+        &mut self,
+        envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
+        _: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+        if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
+            worktree.update(cx, |worktree, cx| {
+                worktree
+                    .as_remote_mut()
+                    .unwrap()
+                    .update_diagnostic_summary(envelope, cx);
+            });
+        }
+        Ok(())
+    }
+
+    fn handle_disk_based_diagnostics_updating(
+        &mut self,
+        envelope: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
+        _: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+        if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
+            worktree.update(cx, |worktree, cx| {
+                worktree
+                    .as_remote()
+                    .unwrap()
+                    .disk_based_diagnostics_updating(cx);
+            });
+        }
+        Ok(())
+    }
+
+    fn handle_disk_based_diagnostics_updated(
+        &mut self,
+        envelope: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
+        _: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+        if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
+            worktree.update(cx, |worktree, cx| {
+                worktree
+                    .as_remote()
+                    .unwrap()
+                    .disk_based_diagnostics_updated(cx);
+            });
+        }
+        Ok(())
+    }
+
     pub fn handle_update_buffer(
         &mut self,
         envelope: TypedEnvelope<proto::UpdateBuffer>,

crates/project/src/worktree.rs 🔗

@@ -7,8 +7,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Context, Result};
 use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
 use clock::ReplicaId;
-use collections::{hash_map, HashMap};
-use collections::{BTreeMap, HashSet};
+use collections::{hash_map, HashMap, HashSet};
 use futures::{Stream, StreamExt};
 use fuzzy::CharBag;
 use gpui::{
@@ -35,7 +34,6 @@ use std::{
     ffi::{OsStr, OsString},
     fmt,
     future::Future,
-    mem,
     ops::{Deref, Range},
     path::{Path, PathBuf},
     sync::{
@@ -44,14 +42,12 @@ use std::{
     },
     time::{Duration, SystemTime},
 };
-use sum_tree::Bias;
+use sum_tree::{Bias, TreeMap};
 use sum_tree::{Edit, SeekTarget, SumTree};
 use util::{post_inc, ResultExt, TryFutureExt};
 
 lazy_static! {
     static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
-    static ref DIAGNOSTIC_PROVIDER_NAME: Arc<str> = Arc::from("diagnostic_source");
-    static ref LSP_PROVIDER_NAME: Arc<str> = Arc::from("lsp");
 }
 
 #[derive(Clone, Debug)]
@@ -69,8 +65,10 @@ pub enum Worktree {
     Remote(RemoteWorktree),
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug, Eq, PartialEq)]
 pub enum Event {
+    DiskBasedDiagnosticsUpdating,
+    DiskBasedDiagnosticsUpdated,
     DiagnosticsUpdated(Arc<Path>),
 }
 
@@ -143,7 +141,7 @@ impl Worktree {
             .map(|c| c.to_ascii_lowercase())
             .collect();
         let root_name = worktree.root_name.clone();
-        let (entries_by_path, entries_by_id) = cx
+        let (entries_by_path, entries_by_id, diagnostic_summaries) = cx
             .background()
             .spawn(async move {
                 let mut entries_by_path_edits = Vec::new();
@@ -167,7 +165,22 @@ impl Worktree {
                 let mut entries_by_id = SumTree::new();
                 entries_by_path.edit(entries_by_path_edits, &());
                 entries_by_id.edit(entries_by_id_edits, &());
-                (entries_by_path, entries_by_id)
+
+                let diagnostic_summaries = TreeMap::from_ordered_entries(
+                    worktree.diagnostic_summaries.into_iter().map(|summary| {
+                        (
+                            PathKey(PathBuf::from(summary.path).into()),
+                            DiagnosticSummary {
+                                error_count: summary.error_count as usize,
+                                warning_count: summary.warning_count as usize,
+                                info_count: summary.info_count as usize,
+                                hint_count: summary.hint_count as usize,
+                            },
+                        )
+                    }),
+                );
+
+                (entries_by_path, entries_by_id, diagnostic_summaries)
             })
             .await;
 
@@ -224,10 +237,10 @@ impl Worktree {
                     client: client.clone(),
                     loading_buffers: Default::default(),
                     open_buffers: Default::default(),
-                    diagnostic_summaries: Default::default(),
                     queued_operations: Default::default(),
                     languages,
                     user_store,
+                    diagnostic_summaries,
                 })
             })
         });
@@ -352,7 +365,7 @@ impl Worktree {
             Worktree::Remote(worktree) => &worktree.diagnostic_summaries,
         }
         .iter()
-        .map(|(path, summary)| (path.clone(), summary.clone()))
+        .map(|(path, summary)| (path.0.clone(), summary.clone()))
     }
 
     pub fn loading_buffers<'a>(&'a mut self) -> &'a mut LoadingBuffers {
@@ -676,9 +689,9 @@ impl Worktree {
         }
     }
 
-    pub fn update_diagnostics_from_lsp(
+    pub fn update_diagnostics(
         &mut self,
-        mut params: lsp::PublishDiagnosticsParams,
+        params: lsp::PublishDiagnosticsParams,
         disk_based_sources: &HashSet<String>,
         cx: &mut ModelContext<Worktree>,
     ) -> Result<()> {
@@ -693,98 +706,103 @@ impl Worktree {
                 .context("path is not within worktree")?,
         );
 
-        let mut group_ids_by_diagnostic_range = HashMap::default();
-        let mut diagnostics_by_group_id = HashMap::default();
         let mut next_group_id = 0;
-        for diagnostic in &mut params.diagnostics {
+        let mut diagnostics = Vec::default();
+        let mut primary_diagnostic_group_ids = HashMap::default();
+        let mut sources_by_group_id = HashMap::default();
+        let mut supporting_diagnostic_severities = HashMap::default();
+        for diagnostic in &params.diagnostics {
             let source = diagnostic.source.as_ref();
-            let code = diagnostic.code.as_ref();
-            let group_id = diagnostic_ranges(&diagnostic, &abs_path)
-                .find_map(|range| group_ids_by_diagnostic_range.get(&(source, code, range)))
-                .copied()
-                .unwrap_or_else(|| {
-                    let group_id = post_inc(&mut next_group_id);
-                    for range in diagnostic_ranges(&diagnostic, &abs_path) {
-                        group_ids_by_diagnostic_range.insert((source, code, range), group_id);
-                    }
-                    group_id
+            let code = diagnostic.code.as_ref().map(|code| match code {
+                lsp::NumberOrString::Number(code) => code.to_string(),
+                lsp::NumberOrString::String(code) => code.clone(),
+            });
+            let range = range_from_lsp(diagnostic.range);
+            let is_supporting = diagnostic
+                .related_information
+                .as_ref()
+                .map_or(false, |infos| {
+                    infos.iter().any(|info| {
+                        primary_diagnostic_group_ids.contains_key(&(
+                            source,
+                            code.clone(),
+                            range_from_lsp(info.location.range),
+                        ))
+                    })
                 });
 
-            diagnostics_by_group_id
-                .entry(group_id)
-                .or_insert(Vec::new())
-                .push(DiagnosticEntry {
-                    range: diagnostic.range.start.to_point_utf16()
-                        ..diagnostic.range.end.to_point_utf16(),
+            if is_supporting {
+                if let Some(severity) = diagnostic.severity {
+                    supporting_diagnostic_severities
+                        .insert((source, code.clone(), range), severity);
+                }
+            } else {
+                let group_id = post_inc(&mut next_group_id);
+                let is_disk_based =
+                    source.map_or(false, |source| disk_based_sources.contains(source));
+
+                sources_by_group_id.insert(group_id, source);
+                primary_diagnostic_group_ids
+                    .insert((source, code.clone(), range.clone()), group_id);
+
+                diagnostics.push(DiagnosticEntry {
+                    range,
                     diagnostic: Diagnostic {
-                        code: diagnostic.code.clone().map(|code| match code {
-                            lsp::NumberOrString::Number(code) => code.to_string(),
-                            lsp::NumberOrString::String(code) => code,
-                        }),
+                        code: code.clone(),
                         severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
-                        message: mem::take(&mut diagnostic.message),
+                        message: diagnostic.message.clone(),
                         group_id,
-                        is_primary: false,
+                        is_primary: true,
                         is_valid: true,
-                        is_disk_based: diagnostic
-                            .source
-                            .as_ref()
-                            .map_or(false, |source| disk_based_sources.contains(source)),
+                        is_disk_based,
                     },
                 });
+                if let Some(infos) = &diagnostic.related_information {
+                    for info in infos {
+                        if info.location.uri == params.uri {
+                            let range = range_from_lsp(info.location.range);
+                            diagnostics.push(DiagnosticEntry {
+                                range,
+                                diagnostic: Diagnostic {
+                                    code: code.clone(),
+                                    severity: DiagnosticSeverity::INFORMATION,
+                                    message: info.message.clone(),
+                                    group_id,
+                                    is_primary: false,
+                                    is_valid: true,
+                                    is_disk_based,
+                                },
+                            });
+                        }
+                    }
+                }
+            }
         }
 
-        let diagnostics = diagnostics_by_group_id
-            .into_values()
-            .flat_map(|mut diagnostics| {
-                let primary = diagnostics
-                    .iter_mut()
-                    .min_by_key(|entry| entry.diagnostic.severity)
-                    .unwrap();
-                primary.diagnostic.is_primary = true;
-                diagnostics
-            })
-            .collect::<Vec<_>>();
-
-        let this = self.as_local_mut().unwrap();
-        for buffer in this.open_buffers.values() {
-            if let Some(buffer) = buffer.upgrade(cx) {
-                if buffer
-                    .read(cx)
-                    .file()
-                    .map_or(false, |file| *file.path() == worktree_path)
-                {
-                    let (remote_id, operation) = buffer.update(cx, |buffer, cx| {
-                        (
-                            buffer.remote_id(),
-                            buffer.update_diagnostics(
-                                LSP_PROVIDER_NAME.clone(),
-                                params.version,
-                                diagnostics.clone(),
-                                cx,
-                            ),
-                        )
-                    });
-                    self.send_buffer_update(remote_id, operation?, cx);
-                    break;
+        for entry in &mut diagnostics {
+            let diagnostic = &mut entry.diagnostic;
+            if !diagnostic.is_primary {
+                let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap();
+                if let Some(&severity) = supporting_diagnostic_severities.get(&(
+                    source,
+                    diagnostic.code.clone(),
+                    entry.range.clone(),
+                )) {
+                    diagnostic.severity = severity;
                 }
             }
         }
 
-        let this = self.as_local_mut().unwrap();
-        this.diagnostic_summaries
-            .insert(worktree_path.clone(), DiagnosticSummary::new(&diagnostics));
-        this.lsp_diagnostics
-            .insert(worktree_path.clone(), diagnostics);
-        cx.emit(Event::DiagnosticsUpdated(worktree_path.clone()));
+        self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?;
         Ok(())
     }
 
-    pub fn update_diagnostics_from_provider(
+    pub fn update_diagnostic_entries(
         &mut self,
-        path: Arc<Path>,
-        diagnostics: Vec<DiagnosticEntry<usize>>,
-        cx: &mut ModelContext<Worktree>,
+        worktree_path: Arc<Path>,
+        version: Option<i32>,
+        diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
+        cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         let this = self.as_local_mut().unwrap();
         for buffer in this.open_buffers.values() {
@@ -792,17 +810,12 @@ impl Worktree {
                 if buffer
                     .read(cx)
                     .file()
-                    .map_or(false, |file| *file.path() == path)
+                    .map_or(false, |file| *file.path() == worktree_path)
                 {
                     let (remote_id, operation) = buffer.update(cx, |buffer, cx| {
                         (
                             buffer.remote_id(),
-                            buffer.update_diagnostics(
-                                DIAGNOSTIC_PROVIDER_NAME.clone(),
-                                None,
-                                diagnostics.clone(),
-                                cx,
-                            ),
+                            buffer.update_diagnostics(version, diagnostics.clone(), cx),
                         )
                     });
                     self.send_buffer_update(remote_id, operation?, cx);
@@ -812,10 +825,40 @@ impl Worktree {
         }
 
         let this = self.as_local_mut().unwrap();
+        let summary = DiagnosticSummary::new(&diagnostics);
         this.diagnostic_summaries
-            .insert(path.clone(), DiagnosticSummary::new(&diagnostics));
-        this.provider_diagnostics.insert(path.clone(), diagnostics);
-        cx.emit(Event::DiagnosticsUpdated(path.clone()));
+            .insert(PathKey(worktree_path.clone()), summary.clone());
+        this.diagnostics.insert(worktree_path.clone(), diagnostics);
+
+        cx.emit(Event::DiagnosticsUpdated(worktree_path.clone()));
+
+        if let Some(share) = this.share.as_ref() {
+            cx.foreground()
+                .spawn({
+                    let client = this.client.clone();
+                    let project_id = share.project_id;
+                    let worktree_id = this.id().to_proto();
+                    let path = worktree_path.to_string_lossy().to_string();
+                    async move {
+                        client
+                            .send(proto::UpdateDiagnosticSummary {
+                                project_id,
+                                worktree_id,
+                                summary: Some(proto::DiagnosticSummary {
+                                    path,
+                                    error_count: summary.error_count as u32,
+                                    warning_count: summary.warning_count as u32,
+                                    info_count: summary.info_count as u32,
+                                    hint_count: summary.hint_count as u32,
+                                }),
+                            })
+                            .await
+                            .log_err()
+                    }
+                })
+                .detach();
+        }
+
         Ok(())
     }
 
@@ -910,9 +953,8 @@ pub struct LocalWorktree {
     loading_buffers: LoadingBuffers,
     open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
     shared_buffers: HashMap<PeerId, HashMap<u64, ModelHandle<Buffer>>>,
-    lsp_diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
-    provider_diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>,
-    diagnostic_summaries: BTreeMap<Arc<Path>, DiagnosticSummary>,
+    diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
+    diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
     queued_operations: Vec<(u64, Operation)>,
     language_registry: Arc<LanguageRegistry>,
     client: Arc<Client>,
@@ -936,10 +978,10 @@ pub struct RemoteWorktree {
     replica_id: ReplicaId,
     loading_buffers: LoadingBuffers,
     open_buffers: HashMap<usize, RemoteBuffer>,
-    diagnostic_summaries: BTreeMap<Arc<Path>, DiagnosticSummary>,
     languages: Arc<LanguageRegistry>,
     user_store: ModelHandle<UserStore>,
     queued_operations: Vec<(u64, Operation)>,
+    diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
 }
 
 type LoadingBuffers = HashMap<
@@ -1018,8 +1060,7 @@ impl LocalWorktree {
                 loading_buffers: Default::default(),
                 open_buffers: Default::default(),
                 shared_buffers: Default::default(),
-                lsp_diagnostics: Default::default(),
-                provider_diagnostics: Default::default(),
+                diagnostics: Default::default(),
                 diagnostic_summaries: Default::default(),
                 queued_operations: Default::default(),
                 language_registry: languages,
@@ -1093,23 +1134,133 @@ impl LocalWorktree {
             .log_err()
             .flatten()
         {
+            enum DiagnosticProgress {
+                Updating,
+                Updated,
+            }
+
             let disk_based_sources = language
                 .disk_based_diagnostic_sources()
                 .cloned()
                 .unwrap_or_default();
+            let disk_based_diagnostics_progress_token =
+                language.disk_based_diagnostics_progress_token().cloned();
             let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
+            let (disk_based_diagnostics_done_tx, disk_based_diagnostics_done_rx) =
+                smol::channel::unbounded();
             language_server
                 .on_notification::<lsp::notification::PublishDiagnostics, _>(move |params| {
                     smol::block_on(diagnostics_tx.send(params)).ok();
                 })
                 .detach();
+            cx.spawn_weak(|this, mut cx| {
+                let has_disk_based_diagnostic_progress_token =
+                    disk_based_diagnostics_progress_token.is_some();
+                let disk_based_diagnostics_done_tx = disk_based_diagnostics_done_tx.clone();
+                async move {
+                    while let Ok(diagnostics) = diagnostics_rx.recv().await {
+                        if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
+                            handle.update(&mut cx, |this, cx| {
+                                if !has_disk_based_diagnostic_progress_token {
+                                    smol::block_on(
+                                        disk_based_diagnostics_done_tx
+                                            .send(DiagnosticProgress::Updating),
+                                    )
+                                    .ok();
+                                }
+                                this.update_diagnostics(diagnostics, &disk_based_sources, cx)
+                                    .log_err();
+                                if !has_disk_based_diagnostic_progress_token {
+                                    smol::block_on(
+                                        disk_based_diagnostics_done_tx
+                                            .send(DiagnosticProgress::Updated),
+                                    )
+                                    .ok();
+                                }
+                            })
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            })
+            .detach();
+
+            let mut pending_disk_based_diagnostics: i32 = 0;
+            language_server
+                .on_notification::<lsp::notification::Progress, _>(move |params| {
+                    let token = match params.token {
+                        lsp::NumberOrString::Number(_) => None,
+                        lsp::NumberOrString::String(token) => Some(token),
+                    };
+
+                    if token == disk_based_diagnostics_progress_token {
+                        match params.value {
+                            lsp::ProgressParamsValue::WorkDone(progress) => match progress {
+                                lsp::WorkDoneProgress::Begin(_) => {
+                                    if pending_disk_based_diagnostics == 0 {
+                                        smol::block_on(
+                                            disk_based_diagnostics_done_tx
+                                                .send(DiagnosticProgress::Updating),
+                                        )
+                                        .ok();
+                                    }
+                                    pending_disk_based_diagnostics += 1;
+                                }
+                                lsp::WorkDoneProgress::End(_) => {
+                                    pending_disk_based_diagnostics -= 1;
+                                    if pending_disk_based_diagnostics == 0 {
+                                        smol::block_on(
+                                            disk_based_diagnostics_done_tx
+                                                .send(DiagnosticProgress::Updated),
+                                        )
+                                        .ok();
+                                    }
+                                }
+                                _ => {}
+                            },
+                        }
+                    }
+                })
+                .detach();
+            let rpc = self.client.clone();
             cx.spawn_weak(|this, mut cx| async move {
-                while let Ok(diagnostics) = diagnostics_rx.recv().await {
+                while let Ok(progress) = disk_based_diagnostics_done_rx.recv().await {
                     if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
-                        handle.update(&mut cx, |this, cx| {
-                            this.update_diagnostics_from_lsp(diagnostics, &disk_based_sources, cx)
-                                .log_err();
-                        });
+                        match progress {
+                            DiagnosticProgress::Updating => {
+                                let message = handle.update(&mut cx, |this, cx| {
+                                    cx.emit(Event::DiskBasedDiagnosticsUpdating);
+                                    let this = this.as_local().unwrap();
+                                    this.share.as_ref().map(|share| {
+                                        proto::DiskBasedDiagnosticsUpdating {
+                                            project_id: share.project_id,
+                                            worktree_id: this.id().to_proto(),
+                                        }
+                                    })
+                                });
+
+                                if let Some(message) = message {
+                                    rpc.send(message).await.log_err();
+                                }
+                            }
+                            DiagnosticProgress::Updated => {
+                                let message = handle.update(&mut cx, |this, cx| {
+                                    cx.emit(Event::DiskBasedDiagnosticsUpdated);
+                                    let this = this.as_local().unwrap();
+                                    this.share.as_ref().map(|share| {
+                                        proto::DiskBasedDiagnosticsUpdated {
+                                            project_id: share.project_id,
+                                            worktree_id: this.id().to_proto(),
+                                        }
+                                    })
+                                });
+
+                                if let Some(message) = message {
+                                    rpc.send(message).await.log_err();
+                                }
+                            }
+                        }
                     } else {
                         break;
                     }
@@ -1158,35 +1309,25 @@ impl LocalWorktree {
                 .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
                 .await?;
 
-            let (lsp_diagnostics, provider_diagnostics, language, language_server) =
-                this.update(&mut cx, |this, cx| {
-                    let this = this.as_local_mut().unwrap();
-                    let lsp_diagnostics = this.lsp_diagnostics.remove(&path);
-                    let provider_diagnostics = this.provider_diagnostics.remove(&path);
-                    let language = this
-                        .language_registry
-                        .select_language(file.full_path())
-                        .cloned();
-                    let server = language
-                        .as_ref()
-                        .and_then(|language| this.register_language(language, cx));
-                    (lsp_diagnostics, provider_diagnostics, language, server)
-                });
+            let (diagnostics, language, language_server) = this.update(&mut cx, |this, cx| {
+                let this = this.as_local_mut().unwrap();
+                let diagnostics = this.diagnostics.get(&path).cloned();
+                let language = this
+                    .language_registry
+                    .select_language(file.full_path())
+                    .cloned();
+                let server = language
+                    .as_ref()
+                    .and_then(|language| this.register_language(language, cx));
+                (diagnostics, language, server)
+            });
 
             let mut buffer_operations = Vec::new();
             let buffer = cx.add_model(|cx| {
                 let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx);
                 buffer.set_language(language, language_server, cx);
-                if let Some(diagnostics) = lsp_diagnostics {
-                    let op = buffer
-                        .update_diagnostics(LSP_PROVIDER_NAME.clone(), None, diagnostics, cx)
-                        .unwrap();
-                    buffer_operations.push(op);
-                }
-                if let Some(diagnostics) = provider_diagnostics {
-                    let op = buffer
-                        .update_diagnostics(DIAGNOSTIC_PROVIDER_NAME.clone(), None, diagnostics, cx)
-                        .unwrap();
+                if let Some(diagnostics) = diagnostics {
+                    let op = buffer.update_diagnostics(None, diagnostics, cx).unwrap();
                     buffer_operations.push(op);
                 }
                 buffer
@@ -1405,10 +1546,11 @@ impl LocalWorktree {
             })
             .detach();
 
+        let diagnostic_summaries = self.diagnostic_summaries.clone();
         let share_message = cx.background().spawn(async move {
             proto::ShareWorktree {
                 project_id,
-                worktree: Some(snapshot.to_proto()),
+                worktree: Some(snapshot.to_proto(&diagnostic_summaries)),
             }
         });
 
@@ -1576,6 +1718,34 @@ impl RemoteWorktree {
         Ok(())
     }
 
+    pub fn update_diagnostic_summary(
+        &mut self,
+        envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
+        cx: &mut ModelContext<Worktree>,
+    ) {
+        if let Some(summary) = envelope.payload.summary {
+            let path: Arc<Path> = Path::new(&summary.path).into();
+            self.diagnostic_summaries.insert(
+                PathKey(path.clone()),
+                DiagnosticSummary {
+                    error_count: summary.error_count as usize,
+                    warning_count: summary.warning_count as usize,
+                    info_count: summary.info_count as usize,
+                    hint_count: summary.hint_count as usize,
+                },
+            );
+            cx.emit(Event::DiagnosticsUpdated(path));
+        }
+    }
+
+    pub fn disk_based_diagnostics_updating(&self, cx: &mut ModelContext<Worktree>) {
+        cx.emit(Event::DiskBasedDiagnosticsUpdating);
+    }
+
+    pub fn disk_based_diagnostics_updated(&self, cx: &mut ModelContext<Worktree>) {
+        cx.emit(Event::DiskBasedDiagnosticsUpdated);
+    }
+
     pub fn remove_collaborator(&mut self, replica_id: ReplicaId, cx: &mut ModelContext<Worktree>) {
         for (_, buffer) in &self.open_buffers {
             if let Some(buffer) = buffer.upgrade(cx) {
@@ -1605,17 +1775,24 @@ impl Snapshot {
         self.id
     }
 
-    pub fn to_proto(&self) -> proto::Worktree {
+    pub fn to_proto(
+        &self,
+        diagnostic_summaries: &TreeMap<PathKey, DiagnosticSummary>,
+    ) -> proto::Worktree {
         let root_name = self.root_name.clone();
         proto::Worktree {
             id: self.id.0 as u64,
             root_name,
             entries: self
                 .entries_by_path
-                .cursor::<()>()
+                .iter()
                 .filter(|e| !e.is_ignored)
                 .map(Into::into)
                 .collect(),
+            diagnostic_summaries: diagnostic_summaries
+                .iter()
+                .map(|(path, summary)| summary.to_proto(path.0.clone()))
+                .collect(),
         }
     }
 
@@ -3013,32 +3190,10 @@ impl ToPointUtf16 for lsp::Position {
     }
 }
 
-fn diagnostic_ranges<'a>(
-    diagnostic: &'a lsp::Diagnostic,
-    abs_path: &'a Path,
-) -> impl 'a + Iterator<Item = Range<PointUtf16>> {
-    diagnostic
-        .related_information
-        .iter()
-        .flatten()
-        .filter_map(move |info| {
-            if info.location.uri.to_file_path().ok()? == abs_path {
-                let info_start = PointUtf16::new(
-                    info.location.range.start.line,
-                    info.location.range.start.character,
-                );
-                let info_end = PointUtf16::new(
-                    info.location.range.end.line,
-                    info.location.range.end.character,
-                );
-                Some(info_start..info_end)
-            } else {
-                None
-            }
-        })
-        .chain(Some(
-            diagnostic.range.start.to_point_utf16()..diagnostic.range.end.to_point_utf16(),
-        ))
+fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
+    let start = PointUtf16::new(range.start.line, range.start.character);
+    let end = PointUtf16::new(range.end.line, range.end.character);
+    start..end
 }
 
 #[cfg(test)]
@@ -3048,6 +3203,7 @@ mod tests {
     use anyhow::Result;
     use client::test::{FakeHttpClient, FakeServer};
     use fs::RealFs;
+    use gpui::test::subscribe;
     use language::{tree_sitter_rust, DiagnosticEntry, LanguageServerConfig};
     use language::{Diagnostic, LanguageConfig};
     use lsp::Url;
@@ -3245,7 +3401,7 @@ mod tests {
         let remote = Worktree::remote(
             1,
             1,
-            initial_snapshot.to_proto(),
+            initial_snapshot.to_proto(&Default::default()),
             Client::new(http_client.clone()),
             user_store,
             Default::default(),
@@ -3697,6 +3853,10 @@ mod tests {
     async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) {
         let (language_server_config, mut fake_server) =
             LanguageServerConfig::fake(cx.background()).await;
+        let progress_token = language_server_config
+            .disk_based_diagnostics_progress_token
+            .clone()
+            .unwrap();
         let mut languages = LanguageRegistry::new();
         languages.add(Arc::new(Language::new(
             LanguageConfig {
@@ -3736,6 +3896,18 @@ mod tests {
             .await
             .unwrap();
 
+        let mut events = subscribe(&tree, &mut cx);
+
+        fake_server.start_progress(&progress_token).await;
+        assert_eq!(
+            events.next().await.unwrap(),
+            Event::DiskBasedDiagnosticsUpdating
+        );
+
+        fake_server.start_progress(&progress_token).await;
+        fake_server.end_progress(&progress_token).await;
+        fake_server.start_progress(&progress_token).await;
+
         fake_server
             .notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
                 uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(),
@@ -3748,6 +3920,17 @@ mod tests {
                 }],
             })
             .await;
+        assert_eq!(
+            events.next().await.unwrap(),
+            Event::DiagnosticsUpdated(Arc::from(Path::new("a.rs")))
+        );
+
+        fake_server.end_progress(&progress_token).await;
+        fake_server.end_progress(&progress_token).await;
+        assert_eq!(
+            events.next().await.unwrap(),
+            Event::DiskBasedDiagnosticsUpdated
+        );
 
         let buffer = tree
             .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx))
@@ -3761,19 +3944,16 @@ mod tests {
                 .collect::<Vec<_>>();
             assert_eq!(
                 diagnostics,
-                &[(
-                    LSP_PROVIDER_NAME.as_ref(),
-                    DiagnosticEntry {
-                        range: Point::new(0, 9)..Point::new(0, 10),
-                        diagnostic: Diagnostic {
-                            severity: lsp::DiagnosticSeverity::ERROR,
-                            message: "undefined variable 'A'".to_string(),
-                            group_id: 0,
-                            is_primary: true,
-                            ..Default::default()
-                        }
+                &[DiagnosticEntry {
+                    range: Point::new(0, 9)..Point::new(0, 10),
+                    diagnostic: Diagnostic {
+                        severity: lsp::DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'A'".to_string(),
+                        group_id: 0,
+                        is_primary: true,
+                        ..Default::default()
                     }
-                )]
+                }]
             )
         });
     }
@@ -3918,7 +4098,7 @@ mod tests {
 
         worktree
             .update(&mut cx, |tree, cx| {
-                tree.update_diagnostics_from_lsp(message, &Default::default(), cx)
+                tree.update_diagnostics(message, &Default::default(), cx)
             })
             .unwrap();
         let buffer = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
@@ -3928,78 +4108,61 @@ mod tests {
                 .diagnostics_in_range::<_, Point>(0..buffer.len())
                 .collect::<Vec<_>>(),
             &[
-                (
-                    LSP_PROVIDER_NAME.as_ref(),
-                    DiagnosticEntry {
-                        range: Point::new(1, 8)..Point::new(1, 9),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::WARNING,
-                            message: "error 1".to_string(),
-                            group_id: 0,
-                            is_primary: true,
-                            ..Default::default()
-                        }
+                DiagnosticEntry {
+                    range: Point::new(1, 8)..Point::new(1, 9),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::WARNING,
+                        message: "error 1".to_string(),
+                        group_id: 0,
+                        is_primary: true,
+                        ..Default::default()
                     }
-                ),
-                (
-                    LSP_PROVIDER_NAME.as_ref(),
-                    DiagnosticEntry {
-                        range: Point::new(1, 8)..Point::new(1, 9),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::HINT,
-                            message: "error 1 hint 1".to_string(),
-                            group_id: 0,
-                            is_primary: false,
-                            ..Default::default()
-                        }
+                },
+                DiagnosticEntry {
+                    range: Point::new(1, 8)..Point::new(1, 9),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 1 hint 1".to_string(),
+                        group_id: 0,
+                        is_primary: false,
+                        ..Default::default()
                     }
-                ),
-                (
-                    LSP_PROVIDER_NAME.as_ref(),
-                    DiagnosticEntry {
-                        range: Point::new(1, 13)..Point::new(1, 15),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::HINT,
-                            message: "error 2 hint 1".to_string(),
-                            group_id: 1,
-                            is_primary: false,
-                            ..Default::default()
-                        }
+                },
+                DiagnosticEntry {
+                    range: Point::new(1, 13)..Point::new(1, 15),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 2 hint 1".to_string(),
+                        group_id: 1,
+                        is_primary: false,
+                        ..Default::default()
                     }
-                ),
-                (
-                    LSP_PROVIDER_NAME.as_ref(),
-                    DiagnosticEntry {
-                        range: Point::new(1, 13)..Point::new(1, 15),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::HINT,
-                            message: "error 2 hint 2".to_string(),
-                            group_id: 1,
-                            is_primary: false,
-                            ..Default::default()
-                        }
+                },
+                DiagnosticEntry {
+                    range: Point::new(1, 13)..Point::new(1, 15),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::HINT,
+                        message: "error 2 hint 2".to_string(),
+                        group_id: 1,
+                        is_primary: false,
+                        ..Default::default()
                     }
-                ),
-                (
-                    LSP_PROVIDER_NAME.as_ref(),
-                    DiagnosticEntry {
-                        range: Point::new(2, 8)..Point::new(2, 17),
-                        diagnostic: Diagnostic {
-                            severity: DiagnosticSeverity::ERROR,
-                            message: "error 2".to_string(),
-                            group_id: 1,
-                            is_primary: true,
-                            ..Default::default()
-                        }
+                },
+                DiagnosticEntry {
+                    range: Point::new(2, 8)..Point::new(2, 17),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "error 2".to_string(),
+                        group_id: 1,
+                        is_primary: true,
+                        ..Default::default()
                     }
-                )
+                }
             ]
         );
 
         assert_eq!(
-            buffer
-                .diagnostic_group::<Point>(&LSP_PROVIDER_NAME, 0)
-                .collect::<Vec<_>>(),
+            buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
             &[
                 DiagnosticEntry {
                     range: Point::new(1, 8)..Point::new(1, 9),
@@ -4024,9 +4187,7 @@ mod tests {
             ]
         );
         assert_eq!(
-            buffer
-                .diagnostic_group::<Point>(&LSP_PROVIDER_NAME, 1)
-                .collect::<Vec<_>>(),
+            buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
             &[
                 DiagnosticEntry {
                     range: Point::new(1, 13)..Point::new(1, 15),

crates/project_panel/Cargo.toml 🔗

@@ -10,6 +10,7 @@ path = "src/project_panel.rs"
 gpui = { path = "../gpui" }
 project = { path = "../project" }
 theme = { path = "../theme" }
+util = { path = "../util" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4.1", features = ["futures-traits"] }
 

crates/project_panel/src/project_panel.rs 🔗

@@ -124,14 +124,14 @@ impl ProjectPanel {
                 if let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) {
                     if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
                         workspace
-                            .open_entry(
+                            .open_path(
                                 ProjectPath {
                                     worktree_id,
                                     path: entry.path.clone(),
                                 },
                                 cx,
                             )
-                            .map(|t| t.detach());
+                            .detach_and_log_err(cx);
                     }
                 }
             }

crates/rpc/proto/zed.proto 🔗

@@ -23,32 +23,34 @@ message Envelope {
 
         RegisterWorktree register_worktree = 17;
         UnregisterWorktree unregister_worktree = 18;
-        ShareWorktree share_worktree = 100;
-        UpdateWorktree update_worktree = 19;
-        UpdateDiagnosticSummary update_diagnostic_summary = 20;
-
-        OpenBuffer open_buffer = 22;
-        OpenBufferResponse open_buffer_response = 23;
-        CloseBuffer close_buffer = 24;
-        UpdateBuffer update_buffer = 25;
-        SaveBuffer save_buffer = 26;
-        BufferSaved buffer_saved = 27;
-
-        GetChannels get_channels = 28;
-        GetChannelsResponse get_channels_response = 29;
-        JoinChannel join_channel = 30;
-        JoinChannelResponse join_channel_response = 31;
-        LeaveChannel leave_channel = 32;
-        SendChannelMessage send_channel_message = 33;
-        SendChannelMessageResponse send_channel_message_response = 34;
-        ChannelMessageSent channel_message_sent = 35;
-        GetChannelMessages get_channel_messages = 36;
-        GetChannelMessagesResponse get_channel_messages_response = 37;
-
-        UpdateContacts update_contacts = 38;
-
-        GetUsers get_users = 39;
-        GetUsersResponse get_users_response = 40;
+        ShareWorktree share_worktree = 19;
+        UpdateWorktree update_worktree = 20;
+        UpdateDiagnosticSummary update_diagnostic_summary = 21;
+        DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 22;
+        DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 23;
+
+        OpenBuffer open_buffer = 24;
+        OpenBufferResponse open_buffer_response = 25;
+        CloseBuffer close_buffer = 26;
+        UpdateBuffer update_buffer = 27;
+        SaveBuffer save_buffer = 28;
+        BufferSaved buffer_saved = 29;
+
+        GetChannels get_channels = 30;
+        GetChannelsResponse get_channels_response = 31;
+        JoinChannel join_channel = 32;
+        JoinChannelResponse join_channel_response = 33;
+        LeaveChannel leave_channel = 34;
+        SendChannelMessage send_channel_message = 35;
+        SendChannelMessageResponse send_channel_message_response = 36;
+        ChannelMessageSent channel_message_sent = 37;
+        GetChannelMessages get_channel_messages = 38;
+        GetChannelMessagesResponse get_channel_messages_response = 39;
+
+        UpdateContacts update_contacts = 40;
+
+        GetUsers get_users = 41;
+        GetUsersResponse get_users_response = 42;
     }
 }
 
@@ -169,9 +171,25 @@ message BufferSaved {
 message UpdateDiagnosticSummary {
     uint64 project_id = 1;
     uint64 worktree_id = 2;
+    DiagnosticSummary summary = 3;
+}
+
+message DiagnosticSummary {
     string path = 3;
     uint32 error_count = 4;
     uint32 warning_count = 5;
+    uint32 info_count = 6;
+    uint32 hint_count = 7;
+}
+
+message DiskBasedDiagnosticsUpdating {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+}
+
+message DiskBasedDiagnosticsUpdated {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
 }
 
 message GetChannels {}
@@ -248,6 +266,7 @@ message Worktree {
     uint64 id = 1;
     string root_name = 2;
     repeated Entry entries = 3;
+    repeated DiagnosticSummary diagnostic_summaries = 4;
 }
 
 message Entry {
@@ -268,7 +287,7 @@ message Buffer {
     repeated UndoMapEntry undo_map = 5;
     repeated VectorClockEntry version = 6;
     repeated SelectionSet selections = 7;
-    repeated DiagnosticSet diagnostic_sets = 8;
+    repeated Diagnostic diagnostics = 8;
     uint32 lamport_timestamp = 9;
     repeated Operation deferred_operations = 10;
 }
@@ -309,15 +328,10 @@ enum Bias {
     Right = 1;
 }
 
-message UpdateDiagnosticSet {
+message UpdateDiagnostics {
     uint32 replica_id = 1;
     uint32 lamport_timestamp = 2;
-    DiagnosticSet diagnostic_set = 3;
-}
-
-message DiagnosticSet {
-    string provider_name = 1;
-    repeated Diagnostic diagnostics = 2;
+    repeated Diagnostic diagnostics = 3;
 }
 
 message Diagnostic {
@@ -345,7 +359,7 @@ message Operation {
         Edit edit = 1;
         Undo undo = 2;
         UpdateSelections update_selections = 3;
-        UpdateDiagnosticSet update_diagnostic_set = 4;
+        UpdateDiagnostics update_diagnostics = 4;
     }
 
     message Edit {

crates/rpc/src/proto.rs 🔗

@@ -125,6 +125,8 @@ messages!(
     BufferSaved,
     ChannelMessageSent,
     CloseBuffer,
+    DiskBasedDiagnosticsUpdated,
+    DiskBasedDiagnosticsUpdating,
     Error,
     GetChannelMessages,
     GetChannelMessagesResponse,
@@ -155,6 +157,7 @@ messages!(
     UnshareProject,
     UpdateBuffer,
     UpdateContacts,
+    UpdateDiagnosticSummary,
     UpdateWorktree,
 );
 
@@ -178,17 +181,20 @@ request_messages!(
 entity_messages!(
     project_id,
     AddProjectCollaborator,
-    RemoveProjectCollaborator,
+    BufferSaved,
+    CloseBuffer,
+    DiskBasedDiagnosticsUpdated,
+    DiskBasedDiagnosticsUpdating,
     JoinProject,
     LeaveProject,
-    BufferSaved,
     OpenBuffer,
-    CloseBuffer,
+    RemoveProjectCollaborator,
     SaveBuffer,
     ShareWorktree,
     UnregisterWorktree,
     UnshareProject,
     UpdateBuffer,
+    UpdateDiagnosticSummary,
     UpdateWorktree,
 );
 

crates/server/src/rpc.rs 🔗

@@ -17,7 +17,7 @@ use rpc::{
     Connection, ConnectionId, Peer, TypedEnvelope,
 };
 use sha1::{Digest as _, Sha1};
-use std::{any::TypeId, future::Future, mem, sync::Arc, time::Instant};
+use std::{any::TypeId, future::Future, mem, path::PathBuf, sync::Arc, time::Instant};
 use store::{Store, Worktree};
 use surf::StatusCode;
 use tide::log;
@@ -71,6 +71,9 @@ impl Server {
             .add_handler(Server::unregister_worktree)
             .add_handler(Server::share_worktree)
             .add_handler(Server::update_worktree)
+            .add_handler(Server::update_diagnostic_summary)
+            .add_handler(Server::disk_based_diagnostics_updating)
+            .add_handler(Server::disk_based_diagnostics_updated)
             .add_handler(Server::open_buffer)
             .add_handler(Server::close_buffer)
             .add_handler(Server::update_buffer)
@@ -300,6 +303,11 @@ impl Server {
                             id: *id,
                             root_name: worktree.root_name.clone(),
                             entries: share.entries.values().cloned().collect(),
+                            diagnostic_summaries: share
+                                .diagnostic_summaries
+                                .values()
+                                .cloned()
+                                .collect(),
                         })
                     })
                     .collect();
@@ -471,11 +479,17 @@ impl Server {
             .map(|entry| (entry.id, entry))
             .collect();
 
+        let diagnostic_summaries = mem::take(&mut worktree.diagnostic_summaries)
+            .into_iter()
+            .map(|summary| (PathBuf::from(summary.path.clone()), summary))
+            .collect();
+
         let contact_user_ids = self.state_mut().share_worktree(
             request.payload.project_id,
             worktree.id,
             request.sender_id,
             entries,
+            diagnostic_summaries,
         );
         if let Some(contact_user_ids) = contact_user_ids {
             self.peer.respond(request.receipt(), proto::Ack {}).await?;
@@ -517,6 +531,64 @@ impl Server {
         Ok(())
     }
 
+    async fn update_diagnostic_summary(
+        mut self: Arc<Server>,
+        request: TypedEnvelope<proto::UpdateDiagnosticSummary>,
+    ) -> tide::Result<()> {
+        let receiver_ids = request
+            .payload
+            .summary
+            .clone()
+            .and_then(|summary| {
+                self.state_mut().update_diagnostic_summary(
+                    request.payload.project_id,
+                    request.payload.worktree_id,
+                    request.sender_id,
+                    summary,
+                )
+            })
+            .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?;
+
+        broadcast(request.sender_id, receiver_ids, |connection_id| {
+            self.peer
+                .forward_send(request.sender_id, connection_id, request.payload.clone())
+        })
+        .await?;
+        Ok(())
+    }
+
+    async fn disk_based_diagnostics_updating(
+        self: Arc<Server>,
+        request: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
+    ) -> tide::Result<()> {
+        let receiver_ids = self
+            .state()
+            .project_connection_ids(request.payload.project_id, request.sender_id)
+            .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?;
+        broadcast(request.sender_id, receiver_ids, |connection_id| {
+            self.peer
+                .forward_send(request.sender_id, connection_id, request.payload.clone())
+        })
+        .await?;
+        Ok(())
+    }
+
+    async fn disk_based_diagnostics_updated(
+        self: Arc<Server>,
+        request: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
+    ) -> tide::Result<()> {
+        let receiver_ids = self
+            .state()
+            .project_connection_ids(request.payload.project_id, request.sender_id)
+            .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?;
+        broadcast(request.sender_id, receiver_ids, |connection_id| {
+            self.peer
+                .forward_send(request.sender_id, connection_id, request.payload.clone())
+        })
+        .await?;
+        Ok(())
+    }
+
     async fn open_buffer(
         self: Arc<Server>,
         request: TypedEnvelope<proto::OpenBuffer>,
@@ -999,7 +1071,7 @@ mod tests {
     };
     use ::rpc::Peer;
     use async_std::task;
-    use gpui::{ModelHandle, TestAppContext};
+    use gpui::{executor, ModelHandle, TestAppContext};
     use parking_lot::Mutex;
     use postage::{mpsc, watch};
     use rpc::PeerId;
@@ -1008,6 +1080,7 @@ mod tests {
     use std::{
         ops::Deref,
         path::Path,
+        rc::Rc,
         sync::{
             atomic::{AtomicBool, Ordering::SeqCst},
             Arc,
@@ -1026,7 +1099,7 @@ mod tests {
             LanguageRegistry, LanguageServerConfig, Point,
         },
         lsp,
-        project::Project,
+        project::{DiagnosticSummary, Project, ProjectPath},
     };
 
     #[gpui::test]
@@ -1037,7 +1110,7 @@ mod tests {
         cx_a.foreground().forbid_parking();
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
 
@@ -1170,7 +1243,7 @@ mod tests {
         cx_a.foreground().forbid_parking();
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
 
@@ -1246,7 +1319,7 @@ mod tests {
         cx_a.foreground().forbid_parking();
 
         // Connect to a server as 3 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
         let client_c = server.create_client(&mut cx_c, "user_c").await;
@@ -1396,7 +1469,7 @@ mod tests {
         let fs = Arc::new(FakeFs::new());
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
 
@@ -1492,7 +1565,7 @@ mod tests {
         let fs = Arc::new(FakeFs::new());
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
 
@@ -1572,7 +1645,7 @@ mod tests {
         let fs = Arc::new(FakeFs::new());
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
 
@@ -1647,7 +1720,7 @@ mod tests {
         let fs = Arc::new(FakeFs::new());
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
 
@@ -1734,7 +1807,7 @@ mod tests {
             )));
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
 
@@ -1767,6 +1840,7 @@ mod tests {
         let project_id = project_a
             .update(&mut cx_a, |project, _| project.next_remote_id())
             .await;
+        let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id());
         project_a
             .update(&mut cx_a, |project, cx| project.share(cx))
             .await
@@ -1782,6 +1856,68 @@ mod tests {
             .unwrap();
 
         // Simulate a language server reporting errors for a file.
+        fake_language_server
+            .notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
+                uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
+                version: None,
+                diagnostics: vec![lsp::Diagnostic {
+                    severity: Some(lsp::DiagnosticSeverity::ERROR),
+                    range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
+                    message: "message 1".to_string(),
+                    ..Default::default()
+                }],
+            })
+            .await;
+
+        // Wait for server to see the diagnostics update.
+        server
+            .condition(|store| {
+                let worktree = store
+                    .project(project_id)
+                    .unwrap()
+                    .worktrees
+                    .get(&worktree_id.to_proto())
+                    .unwrap();
+
+                !worktree
+                    .share
+                    .as_ref()
+                    .unwrap()
+                    .diagnostic_summaries
+                    .is_empty()
+            })
+            .await;
+
+        // Join the worktree as client B.
+        let project_b = Project::remote(
+            project_id,
+            client_b.clone(),
+            client_b.user_store.clone(),
+            lang_registry.clone(),
+            fs.clone(),
+            &mut cx_b.to_async(),
+        )
+        .await
+        .unwrap();
+
+        project_b.read_with(&cx_b, |project, cx| {
+            assert_eq!(
+                project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+                &[(
+                    ProjectPath {
+                        worktree_id,
+                        path: Arc::from(Path::new("a.rs")),
+                    },
+                    DiagnosticSummary {
+                        error_count: 1,
+                        warning_count: 0,
+                        ..Default::default()
+                    },
+                )]
+            )
+        });
+
+        // Simulate a language server reporting more errors for a file.
         fake_language_server
             .notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
                 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
@@ -1806,20 +1942,26 @@ mod tests {
             })
             .await;
 
-        // Join the worktree as client B.
-        let project_b = Project::remote(
-            project_id,
-            client_b.clone(),
-            client_b.user_store.clone(),
-            lang_registry.clone(),
-            fs.clone(),
-            &mut cx_b.to_async(),
-        )
-        .await
-        .unwrap();
-        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+        // Client b gets the updated summaries
+        project_b
+            .condition(&cx_b, |project, cx| {
+                project.diagnostic_summaries(cx).collect::<Vec<_>>()
+                    == &[(
+                        ProjectPath {
+                            worktree_id,
+                            path: Arc::from(Path::new("a.rs")),
+                        },
+                        DiagnosticSummary {
+                            error_count: 1,
+                            warning_count: 1,
+                            ..Default::default()
+                        },
+                    )]
+            })
+            .await;
 
-        // Open the file with the errors.
+        // Open the file with the errors on client B. They should be present.
+        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
         let buffer_b = cx_b
             .background()
             .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx)))
@@ -1831,7 +1973,7 @@ mod tests {
                 buffer
                     .snapshot()
                     .diagnostics_in_range::<_, Point>(0..buffer.len())
-                    .map(|(_, entry)| entry)
+                    .map(|entry| entry)
                     .collect::<Vec<_>>(),
                 &[
                     DiagnosticEntry {
@@ -1864,7 +2006,7 @@ mod tests {
         cx_a.foreground().forbid_parking();
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
 
@@ -2003,7 +2145,7 @@ mod tests {
     async fn test_chat_message_validation(mut cx_a: TestAppContext) {
         cx_a.foreground().forbid_parking();
 
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
 
         let db = &server.app_state.db;
@@ -2064,7 +2206,7 @@ mod tests {
         cx_a.foreground().forbid_parking();
 
         // Connect to a server as 2 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
         let mut status_b = client_b.status();
@@ -2282,7 +2424,7 @@ mod tests {
         let fs = Arc::new(FakeFs::new());
 
         // Connect to a server as 3 clients.
-        let mut server = TestServer::start().await;
+        let mut server = TestServer::start(cx_a.foreground()).await;
         let client_a = server.create_client(&mut cx_a, "user_a").await;
         let client_b = server.create_client(&mut cx_b, "user_b").await;
         let client_c = server.create_client(&mut cx_c, "user_c").await;
@@ -2415,6 +2557,7 @@ mod tests {
         peer: Arc<Peer>,
         app_state: Arc<AppState>,
         server: Arc<Server>,
+        foreground: Rc<executor::Foreground>,
         notifications: mpsc::Receiver<()>,
         connection_killers: Arc<Mutex<HashMap<UserId, watch::Sender<Option<()>>>>>,
         forbid_connections: Arc<AtomicBool>,
@@ -2422,7 +2565,7 @@ mod tests {
     }
 
     impl TestServer {
-        async fn start() -> Self {
+        async fn start(foreground: Rc<executor::Foreground>) -> Self {
             let test_db = TestDb::new();
             let app_state = Self::build_app_state(&test_db).await;
             let peer = Peer::new();
@@ -2432,6 +2575,7 @@ mod tests {
                 peer,
                 app_state,
                 server,
+                foreground,
                 notifications: notifications.1,
                 connection_killers: Default::default(),
                 forbid_connections: Default::default(),
@@ -2547,7 +2691,9 @@ mod tests {
         {
             async_std::future::timeout(Duration::from_millis(500), async {
                 while !(predicate)(&*self.server.store.read()) {
+                    self.foreground.start_waiting();
                     self.notifications.recv().await;
+                    self.foreground.finish_waiting();
                 }
             })
             .await

crates/server/src/rpc/store.rs 🔗

@@ -1,8 +1,8 @@
 use crate::db::{ChannelId, UserId};
 use anyhow::anyhow;
-use collections::{HashMap, HashSet};
+use collections::{BTreeMap, HashMap, HashSet};
 use rpc::{proto, ConnectionId};
-use std::collections::hash_map;
+use std::{collections::hash_map, path::PathBuf};
 
 #[derive(Default)]
 pub struct Store {
@@ -41,6 +41,7 @@ pub struct ProjectShare {
 
 pub struct WorktreeShare {
     pub entries: HashMap<u64, proto::Entry>,
+    pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
 }
 
 #[derive(Default)]
@@ -385,17 +386,42 @@ impl Store {
         worktree_id: u64,
         connection_id: ConnectionId,
         entries: HashMap<u64, proto::Entry>,
+        diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
     ) -> Option<Vec<UserId>> {
         let project = self.projects.get_mut(&project_id)?;
         let worktree = project.worktrees.get_mut(&worktree_id)?;
         if project.host_connection_id == connection_id && project.share.is_some() {
-            worktree.share = Some(WorktreeShare { entries });
+            worktree.share = Some(WorktreeShare {
+                entries,
+                diagnostic_summaries,
+            });
             Some(project.authorized_user_ids())
         } else {
             None
         }
     }
 
+    pub fn update_diagnostic_summary(
+        &mut self,
+        project_id: u64,
+        worktree_id: u64,
+        connection_id: ConnectionId,
+        summary: proto::DiagnosticSummary,
+    ) -> Option<Vec<ConnectionId>> {
+        let project = self.projects.get_mut(&project_id)?;
+        let worktree = project.worktrees.get_mut(&worktree_id)?;
+        if project.host_connection_id == connection_id {
+            if let Some(share) = worktree.share.as_mut() {
+                share
+                    .diagnostic_summaries
+                    .insert(summary.path.clone().into(), summary);
+                return Some(project.connection_ids());
+            }
+        }
+
+        None
+    }
+
     pub fn join_project(
         &mut self,
         connection_id: ConnectionId,
@@ -497,6 +523,11 @@ impl Store {
         Some(self.channels.get(&channel_id)?.connection_ids())
     }
 
+    #[cfg(test)]
+    pub fn project(&self, project_id: u64) -> Option<&Project> {
+        self.projects.get(&project_id)
+    }
+
     pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Option<&Project> {
         let project = self.projects.get(&project_id)?;
         if project.host_connection_id == connection_id

crates/sum_tree/src/tree_map.rs 🔗

@@ -21,6 +21,16 @@ pub struct MapKey<K>(K);
 pub struct MapKeyRef<'a, K>(Option<&'a K>);
 
 impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
+    pub fn from_ordered_entries(entries: impl IntoIterator<Item = (K, V)>) -> Self {
+        let tree = SumTree::from_iter(
+            entries
+                .into_iter()
+                .map(|(key, value)| MapEntry { key, value }),
+            &(),
+        );
+        Self(tree)
+    }
+
     pub fn get<'a>(&self, key: &'a K) -> Option<&V> {
         let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
         cursor.seek(&MapKeyRef(Some(key)), Bias::Left, &());

crates/text/src/text.rs 🔗

@@ -1536,7 +1536,7 @@ impl BufferSnapshot {
                 insertion_cursor.prev(&());
             }
             let insertion = insertion_cursor.item().expect("invalid insertion");
-            debug_assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion");
+            assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion");
 
             fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left, &None);
             let fragment = fragment_cursor.item().unwrap();
@@ -1578,7 +1578,7 @@ impl BufferSnapshot {
                 insertion_cursor.prev(&());
             }
             let insertion = insertion_cursor.item().expect("invalid insertion");
-            debug_assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion");
+            assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion");
 
             let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>();
             fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left, &None);

crates/theme/src/theme.rs 🔗

@@ -24,6 +24,7 @@ pub struct Theme {
     pub project_panel: ProjectPanel,
     pub selector: Selector,
     pub editor: EditorStyle,
+    pub project_diagnostics: ProjectDiagnostics,
 }
 
 #[derive(Deserialize, Default)]
@@ -226,6 +227,14 @@ pub struct ContainedLabel {
     pub label: LabelStyle,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct ProjectDiagnostics {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    pub empty_message: TextStyle,
+    pub status_bar_item: ContainedText,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct EditorStyle {
     pub text: TextStyle,
@@ -253,6 +262,8 @@ pub struct EditorStyle {
 #[derive(Copy, Clone, Deserialize, Default)]
 pub struct DiagnosticStyle {
     pub text: Color,
+    #[serde(default)]
+    pub header: ContainerStyle,
 }
 
 #[derive(Clone, Copy, Default, Deserialize)]

crates/theme_selector/src/theme_selector.rs 🔗

@@ -293,7 +293,7 @@ impl View for ThemeSelector {
                 Container::new(
                     Flex::new(Axis::Vertical)
                         .with_child(ChildView::new(self.query_editor.id()).boxed())
-                        .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
+                        .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
                         .boxed(),
                 )
                 .with_style(settings.theme.selector.container)

crates/workspace/Cargo.toml 🔗

@@ -12,6 +12,7 @@ test-support = ["client/test-support", "project/test-support"]
 [dependencies]
 client = { path = "../client" }
 clock = { path = "../clock" }
+collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }

crates/workspace/src/pane.rs 🔗

@@ -1,15 +1,14 @@
 use super::{ItemViewHandle, SplitDirection};
-use crate::Settings;
+use crate::{ItemHandle, Settings, Workspace};
 use gpui::{
     action,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
     platform::CursorStyle,
-    Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
+    Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
 };
 use postage::watch;
-use project::ProjectPath;
 use std::cmp;
 
 action!(Split, SplitDirection);
@@ -70,7 +69,7 @@ pub struct TabState {
 }
 
 pub struct Pane {
-    items: Vec<Box<dyn ItemViewHandle>>,
+    item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
     active_item: usize,
     settings: watch::Receiver<Settings>,
 }
@@ -78,7 +77,7 @@ pub struct Pane {
 impl Pane {
     pub fn new(settings: watch::Receiver<Settings>) -> Self {
         Self {
-            items: Vec::new(),
+            item_views: Vec::new(),
             active_item: 0,
             settings,
         }
@@ -88,43 +87,70 @@ impl Pane {
         cx.emit(Event::Activate);
     }
 
-    pub fn add_item(&mut self, item: Box<dyn ItemViewHandle>, cx: &mut ViewContext<Self>) -> usize {
-        let item_idx = cmp::min(self.active_item + 1, self.items.len());
-        self.items.insert(item_idx, item);
+    pub fn open_item<T>(
+        &mut self,
+        item_handle: T,
+        workspace: &Workspace,
+        cx: &mut ViewContext<Self>,
+    ) -> Box<dyn ItemViewHandle>
+    where
+        T: 'static + ItemHandle,
+    {
+        for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() {
+            if *item_id == item_handle.id() {
+                let item_view = item_view.boxed_clone();
+                self.activate_item(ix, cx);
+                return item_view;
+            }
+        }
+
+        let item_view = item_handle.add_view(cx.window_id(), workspace, cx);
+        self.add_item_view(item_view.boxed_clone(), cx);
+        item_view
+    }
+
+    pub fn add_item_view(
+        &mut self,
+        item_view: Box<dyn ItemViewHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        item_view.added_to_pane(cx);
+        let item_idx = cmp::min(self.active_item + 1, self.item_views.len());
+        self.item_views
+            .insert(item_idx, (item_view.item_handle(cx).id(), item_view));
+        self.activate_item(item_idx, cx);
         cx.notify();
-        item_idx
     }
 
-    pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
-        &self.items
+    pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
+        let item_id = item.id();
+        self.item_views
+            .iter()
+            .any(|(existing_item_id, _)| *existing_item_id == item_id)
+    }
+
+    pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
+        self.item_views.iter().map(|(_, view)| view)
     }
 
     pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
-        self.items.get(self.active_item).cloned()
+        self.item_views
+            .get(self.active_item)
+            .map(|(_, view)| view.clone())
     }
 
-    pub fn activate_entry(
-        &mut self,
-        project_path: ProjectPath,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Box<dyn ItemViewHandle>> {
-        if let Some(index) = self.items.iter().position(|item| {
-            item.project_path(cx.as_ref())
-                .map_or(false, |item_path| item_path == project_path)
-        }) {
-            self.activate_item(index, cx);
-            self.items.get(index).map(|handle| handle.boxed_clone())
-        } else {
-            None
-        }
+    pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
+        self.item_views
+            .iter()
+            .position(|(_, i)| i.id() == item_view.id())
     }
 
-    pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
-        self.items.iter().position(|i| i.id() == item.id())
+    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
+        self.item_views.iter().position(|(id, _)| *id == item.id())
     }
 
     pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
-        if index < self.items.len() {
+        if index < self.item_views.len() {
             self.active_item = index;
             self.focus_active_item(cx);
             cx.notify();
@@ -134,15 +160,15 @@ impl Pane {
     pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
         if self.active_item > 0 {
             self.active_item -= 1;
-        } else if self.items.len() > 0 {
-            self.active_item = self.items.len() - 1;
+        } else if self.item_views.len() > 0 {
+            self.active_item = self.item_views.len() - 1;
         }
         self.focus_active_item(cx);
         cx.notify();
     }
 
     pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
-        if self.active_item + 1 < self.items.len() {
+        if self.active_item + 1 < self.item_views.len() {
             self.active_item += 1;
         } else {
             self.active_item = 0;
@@ -152,15 +178,15 @@ impl Pane {
     }
 
     pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
-        if !self.items.is_empty() {
-            self.close_item(self.items[self.active_item].id(), cx)
+        if !self.item_views.is_empty() {
+            self.close_item(self.item_views[self.active_item].1.id(), cx)
         }
     }
 
     pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
-        self.items.retain(|item| item.id() != item_id);
-        self.active_item = cmp::min(self.active_item, self.items.len().saturating_sub(1));
-        if self.items.is_empty() {
+        self.item_views.retain(|(_, item)| item.id() != item_id);
+        self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1));
+        if self.item_views.is_empty() {
             cx.emit(Event::Remove);
         }
         cx.notify();
@@ -183,11 +209,11 @@ impl Pane {
         enum Tabs {}
         let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
             let mut row = Flex::row();
-            for (ix, item) in self.items.iter().enumerate() {
+            for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
                 let is_active = ix == self.active_item;
 
                 row.add_child({
-                    let mut title = item.title(cx);
+                    let mut title = item_view.title(cx);
                     if title.len() > MAX_TAB_TITLE_LEN {
                         let mut truncated_len = MAX_TAB_TITLE_LEN;
                         while !title.is_char_boundary(truncated_len) {
@@ -212,9 +238,9 @@ impl Pane {
                                 .with_child(
                                     Align::new({
                                         let diameter = 7.0;
-                                        let icon_color = if item.has_conflict(cx) {
+                                        let icon_color = if item_view.has_conflict(cx) {
                                             Some(style.icon_conflict)
-                                        } else if item.is_dirty(cx) {
+                                        } else if item_view.is_dirty(cx) {
                                             Some(style.icon_dirty)
                                         } else {
                                             None
@@ -271,7 +297,7 @@ impl Pane {
                                 .with_child(
                                     Align::new(
                                         ConstrainedBox::new(if mouse_state.hovered {
-                                            let item_id = item.id();
+                                            let item_id = item_view.id();
                                             enum TabCloseButton {}
                                             let icon = Svg::new("icons/x.svg");
                                             MouseEventHandler::new::<TabCloseButton, _, _, _>(
@@ -314,13 +340,11 @@ impl Pane {
             }
 
             row.add_child(
-                Expanded::new(
-                    0.0,
-                    Container::new(Empty::new().boxed())
-                        .with_border(theme.workspace.tab.container.border)
-                        .boxed(),
-                )
-                .named("filler"),
+                Empty::new()
+                    .contained()
+                    .with_border(theme.workspace.tab.container.border)
+                    .flexible(0., true)
+                    .named("filler"),
             );
 
             row.boxed()
@@ -345,7 +369,7 @@ impl View for Pane {
         if let Some(active_item) = self.active_item() {
             Flex::column()
                 .with_child(self.render_tabs(cx))
-                .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
+                .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
                 .named("pane")
         } else {
             Empty::new().named("pane")
@@ -356,17 +380,3 @@ impl View for Pane {
         self.focus_active_item(cx);
     }
 }
-
-pub trait PaneHandle {
-    fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext);
-}
-
-impl PaneHandle for ViewHandle<Pane> {
-    fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext) {
-        item.set_parent_pane(self, cx);
-        self.update(cx, |pane, cx| {
-            let item_idx = pane.add_item(item, cx);
-            pane.activate_item(item_idx, cx);
-        });
-    }
-}

crates/workspace/src/pane_group.rs 🔗

@@ -183,7 +183,7 @@ impl PaneAxis {
                     member = Container::new(member).with_border(border).boxed();
                 }
 
-                Expanded::new(1.0, member).boxed()
+                Flexible::new(1.0, true, member).boxed()
             }))
             .boxed()
     }

crates/workspace/src/sidebar.rs 🔗

@@ -135,19 +135,16 @@ impl Sidebar {
             }
 
             container.add_child(
-                Flexible::new(
-                    1.,
-                    Hook::new(
-                        ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
-                            .with_max_width(*self.width.borrow())
-                            .boxed(),
-                    )
-                    .on_after_layout({
-                        let width = self.width.clone();
-                        move |size, _| *width.borrow_mut() = size.x()
-                    })
-                    .boxed(),
+                Hook::new(
+                    ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
+                        .with_max_width(*self.width.borrow())
+                        .boxed(),
                 )
+                .on_after_layout({
+                    let width = self.width.clone();
+                    move |size, _| *width.borrow_mut() = size.x()
+                })
+                .flexible(1., false)
                 .boxed(),
             );
             if matches!(self.side, Side::Left) {

crates/workspace/src/status_bar.rs 🔗

@@ -47,7 +47,7 @@ impl View for StatusBar {
                     .iter()
                     .map(|i| ChildView::new(i.id()).aligned().boxed()),
             )
-            .with_child(Empty::new().expanded(1.).boxed())
+            .with_child(Empty::new().flexible(1., true).boxed())
             .with_children(
                 self.right_items
                     .iter()

crates/workspace/src/workspace.rs 🔗

@@ -7,6 +7,7 @@ mod status_bar;
 use anyhow::{anyhow, Result};
 use client::{Authenticate, ChannelList, Client, User, UserStore};
 use clock::ReplicaId;
+use collections::HashSet;
 use gpui::{
     action,
     color::Color,
@@ -15,9 +16,9 @@ use gpui::{
     json::{self, to_string_pretty, ToJson},
     keymap::Binding,
     platform::{CursorStyle, WindowOptions},
-    AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, MutableAppContext,
-    PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
-    WeakModelHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle,
+    MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext,
+    ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
 use log::error;
@@ -32,6 +33,7 @@ use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
     future::Future,
+    hash::{Hash, Hasher},
     path::{Path, PathBuf},
     sync::Arc,
 };
@@ -94,7 +96,7 @@ pub struct AppState {
     pub user_store: ModelHandle<client::UserStore>,
     pub fs: Arc<dyn fs::Fs>,
     pub channel_list: ModelHandle<client::ChannelList>,
-    pub entry_openers: Arc<[Box<dyn EntryOpener>]>,
+    pub path_openers: Arc<[Box<dyn PathOpener>]>,
     pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>,
     pub build_workspace: &'static dyn Fn(
         ModelHandle<Project>,
@@ -115,7 +117,7 @@ pub struct JoinProjectParams {
     pub app_state: Arc<AppState>,
 }
 
-pub trait EntryOpener {
+pub trait PathOpener {
     fn open(
         &self,
         worktree: &mut Worktree,
@@ -129,7 +131,7 @@ pub trait Item: Entity + Sized {
 
     fn build_view(
         handle: ModelHandle<Self>,
-        settings: watch::Receiver<Settings>,
+        workspace: &Workspace,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View;
 
@@ -137,6 +139,9 @@ pub trait Item: Entity + Sized {
 }
 
 pub trait ItemView: View {
+    type ItemHandle: ItemHandle;
+
+    fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle;
     fn title(&self, cx: &AppContext) -> String;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
     fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
@@ -172,27 +177,31 @@ pub trait ItemView: View {
 }
 
 pub trait ItemHandle: Send + Sync {
+    fn id(&self) -> usize;
     fn add_view(
         &self,
         window_id: usize,
-        settings: watch::Receiver<Settings>,
+        workspace: &Workspace,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle>;
     fn boxed_clone(&self) -> Box<dyn ItemHandle>;
     fn downgrade(&self) -> Box<dyn WeakItemHandle>;
+    fn to_any(&self) -> AnyModelHandle;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
 }
 
 pub trait WeakItemHandle {
+    fn id(&self) -> usize;
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
 }
 
 pub trait ItemViewHandle {
+    fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
     fn title(&self, cx: &AppContext) -> String;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
     fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
     fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
-    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext);
+    fn added_to_pane(&self, cx: &mut ViewContext<Pane>);
     fn id(&self) -> usize;
     fn to_any(&self) -> AnyViewHandle;
     fn is_dirty(&self, cx: &AppContext) -> bool;
@@ -209,13 +218,17 @@ pub trait ItemViewHandle {
 }
 
 impl<T: Item> ItemHandle for ModelHandle<T> {
+    fn id(&self) -> usize {
+        self.id()
+    }
+
     fn add_view(
         &self,
         window_id: usize,
-        settings: watch::Receiver<Settings>,
+        workspace: &Workspace,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), settings, cx)))
+        Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), workspace, cx)))
     }
 
     fn boxed_clone(&self) -> Box<dyn ItemHandle> {
@@ -226,19 +239,27 @@ impl<T: Item> ItemHandle for ModelHandle<T> {
         Box::new(self.downgrade())
     }
 
+    fn to_any(&self) -> AnyModelHandle {
+        self.clone().into()
+    }
+
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
         self.read(cx).project_path()
     }
 }
 
 impl ItemHandle for Box<dyn ItemHandle> {
+    fn id(&self) -> usize {
+        ItemHandle::id(self.as_ref())
+    }
+
     fn add_view(
         &self,
         window_id: usize,
-        settings: watch::Receiver<Settings>,
+        workspace: &Workspace,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        ItemHandle::add_view(self.as_ref(), window_id, settings, cx)
+        ItemHandle::add_view(self.as_ref(), window_id, workspace, cx)
     }
 
     fn boxed_clone(&self) -> Box<dyn ItemHandle> {
@@ -249,18 +270,44 @@ impl ItemHandle for Box<dyn ItemHandle> {
         self.as_ref().downgrade()
     }
 
+    fn to_any(&self) -> AnyModelHandle {
+        self.as_ref().to_any()
+    }
+
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
         self.as_ref().project_path(cx)
     }
 }
 
 impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
+    fn id(&self) -> usize {
+        WeakModelHandle::id(self)
+    }
+
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
         WeakModelHandle::<T>::upgrade(*self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
     }
 }
 
+impl Hash for Box<dyn WeakItemHandle> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.id().hash(state);
+    }
+}
+
+impl PartialEq for Box<dyn WeakItemHandle> {
+    fn eq(&self, other: &Self) -> bool {
+        self.id() == other.id()
+    }
+}
+
+impl Eq for Box<dyn WeakItemHandle> {}
+
 impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
+    fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
+        Box::new(self.read(cx).item_handle(cx))
+    }
+
     fn title(&self, cx: &AppContext) -> String {
         self.read(cx).title(cx)
     }
@@ -280,25 +327,23 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
     }
 
-    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext) {
-        pane.update(cx, |_, cx| {
-            cx.subscribe(self, |pane, item, event, cx| {
-                if T::should_close_item_on_event(event) {
-                    pane.close_item(item.id(), cx);
-                    return;
-                }
-                if T::should_activate_item_on_event(event) {
-                    if let Some(ix) = pane.item_index(&item) {
-                        pane.activate_item(ix, cx);
-                        pane.activate(cx);
-                    }
-                }
-                if T::should_update_tab_on_event(event) {
-                    cx.notify()
+    fn added_to_pane(&self, cx: &mut ViewContext<Pane>) {
+        cx.subscribe(self, |pane, item, event, cx| {
+            if T::should_close_item_on_event(event) {
+                pane.close_item(item.id(), cx);
+                return;
+            }
+            if T::should_activate_item_on_event(event) {
+                if let Some(ix) = pane.index_for_item_view(&item) {
+                    pane.activate_item(ix, cx);
+                    pane.activate(cx);
                 }
-            })
-            .detach();
-        });
+            }
+            if T::should_update_tab_on_event(event) {
+                cx.notify()
+            }
+        })
+        .detach();
     }
 
     fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
@@ -360,7 +405,7 @@ pub struct WorkspaceParams {
     pub settings: watch::Receiver<Settings>,
     pub user_store: ModelHandle<UserStore>,
     pub channel_list: ModelHandle<ChannelList>,
-    pub entry_openers: Arc<[Box<dyn EntryOpener>]>,
+    pub path_openers: Arc<[Box<dyn PathOpener>]>,
 }
 
 impl WorkspaceParams {
@@ -392,7 +437,7 @@ impl WorkspaceParams {
             languages,
             settings: watch::channel_with(settings).1,
             user_store,
-            entry_openers: Arc::from([]),
+            path_openers: Arc::from([]),
         }
     }
 
@@ -412,13 +457,14 @@ impl WorkspaceParams {
             settings: app_state.settings.clone(),
             user_store: app_state.user_store.clone(),
             channel_list: app_state.channel_list.clone(),
-            entry_openers: app_state.entry_openers.clone(),
+            path_openers: app_state.path_openers.clone(),
         }
     }
 }
 
 pub struct Workspace {
     pub settings: watch::Receiver<Settings>,
+    weak_self: WeakViewHandle<Self>,
     client: Arc<Client>,
     user_store: ModelHandle<client::UserStore>,
     fs: Arc<dyn Fs>,
@@ -430,8 +476,8 @@ pub struct Workspace {
     active_pane: ViewHandle<Pane>,
     status_bar: ViewHandle<StatusBar>,
     project: ModelHandle<Project>,
-    entry_openers: Arc<[Box<dyn EntryOpener>]>,
-    items: Vec<Box<dyn WeakItemHandle>>,
+    path_openers: Arc<[Box<dyn PathOpener>]>,
+    items: HashSet<Box<dyn WeakItemHandle>>,
     _observe_current_user: Task<()>,
 }
 
@@ -473,6 +519,7 @@ impl Workspace {
 
         Workspace {
             modal: None,
+            weak_self: cx.weak_handle(),
             center: PaneGroup::new(pane.id()),
             panes: vec![pane.clone()],
             active_pane: pane.clone(),
@@ -484,12 +531,20 @@ impl Workspace {
             left_sidebar: Sidebar::new(Side::Left),
             right_sidebar: Sidebar::new(Side::Right),
             project: params.project.clone(),
-            entry_openers: params.entry_openers.clone(),
+            path_openers: params.path_openers.clone(),
             items: Default::default(),
             _observe_current_user,
         }
     }
 
+    pub fn weak_handle(&self) -> WeakViewHandle<Self> {
+        self.weak_self.clone()
+    }
+
+    pub fn settings(&self) -> watch::Receiver<Settings> {
+        self.settings.clone()
+    }
+
     pub fn left_sidebar_mut(&mut self) -> &mut Sidebar {
         &mut self.left_sidebar
     }
@@ -560,13 +615,13 @@ impl Workspace {
                     async move {
                         let project_path = project_path.await.ok()?;
                         if fs.is_file(&abs_path).await {
-                            if let Some(entry) =
-                                this.update(&mut cx, |this, cx| this.open_entry(project_path, cx))
-                            {
-                                return Some(entry.await);
-                            }
+                            Some(
+                                this.update(&mut cx, |this, cx| this.open_path(project_path, cx))
+                                    .await,
+                            )
+                        } else {
+                            None
                         }
-                        None
                     }
                 })
             })
@@ -665,104 +720,59 @@ impl Workspace {
     }
 
     #[must_use]
-    pub fn open_entry(
+    pub fn open_path(
         &mut self,
-        project_path: ProjectPath,
+        path: ProjectPath,
         cx: &mut ViewContext<Self>,
-    ) -> Option<Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>>> {
-        let pane = self.active_pane().clone();
-        if let Some(existing_item) =
-            self.activate_or_open_existing_entry(project_path.clone(), &pane, cx)
-        {
-            return Some(cx.foreground().spawn(async move { Ok(existing_item) }));
+    ) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
+        if let Some(existing_item) = self.item_for_path(&path, cx) {
+            return Task::ready(Ok(self.open_item(existing_item, cx)));
         }
 
-        let worktree = match self
-            .project
-            .read(cx)
-            .worktree_for_id(project_path.worktree_id, cx)
-        {
+        let worktree = match self.project.read(cx).worktree_for_id(path.worktree_id, cx) {
             Some(worktree) => worktree,
             None => {
-                log::error!("worktree {} does not exist", project_path.worktree_id);
-                return None;
+                return Task::ready(Err(Arc::new(anyhow!(
+                    "worktree {} does not exist",
+                    path.worktree_id
+                ))));
             }
         };
 
-        let project_path = project_path.clone();
-        let entry_openers = self.entry_openers.clone();
-        let task = worktree.update(cx, |worktree, cx| {
-            for opener in entry_openers.iter() {
+        let project_path = path.clone();
+        let path_openers = self.path_openers.clone();
+        let open_task = worktree.update(cx, |worktree, cx| {
+            for opener in path_openers.iter() {
                 if let Some(task) = opener.open(worktree, project_path.clone(), cx) {
-                    return Some(task);
+                    return task;
                 }
             }
-            log::error!("no opener for path {:?} found", project_path);
-            None
-        })?;
+            Task::ready(Err(anyhow!("no opener found for path {:?}", project_path)))
+        });
 
-        let pane = pane.downgrade();
-        Some(cx.spawn(|this, mut cx| async move {
-            let load_result = task.await;
+        let pane = self.active_pane().clone().downgrade();
+        cx.spawn(|this, mut cx| async move {
+            let item = open_task.await?;
             this.update(&mut cx, |this, cx| {
                 let pane = pane
                     .upgrade(&cx)
                     .ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
-                let item = load_result?;
-
-                // By the time loading finishes, the entry could have been already added
-                // to the pane. If it was, we activate it, otherwise we'll store the
-                // item and add a new view for it.
-                if let Some(existing) =
-                    this.activate_or_open_existing_entry(project_path, &pane, cx)
-                {
-                    Ok(existing)
-                } else {
-                    Ok(this.add_item(item, cx))
-                }
+                Ok(this.open_item_in_pane(item, &pane, cx))
             })
-        }))
+        })
     }
 
-    fn activate_or_open_existing_entry(
-        &mut self,
-        project_path: ProjectPath,
-        pane: &ViewHandle<Pane>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Box<dyn ItemViewHandle>> {
-        // If the pane contains a view for this file, then activate
-        // that item view.
-        if let Some(existing_item_view) =
-            pane.update(cx, |pane, cx| pane.activate_entry(project_path.clone(), cx))
-        {
-            return Some(existing_item_view);
-        }
+    fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
+        self.items
+            .iter()
+            .filter_map(|i| i.upgrade(cx))
+            .find(|i| i.project_path(cx).as_ref() == Some(path))
+    }
 
-        // Otherwise, if this file is already open somewhere in the workspace,
-        // then add another view for it.
-        let settings = self.settings.clone();
-        let mut view_for_existing_item = None;
-        self.items.retain(|item| {
-            if let Some(item) = item.upgrade(cx) {
-                if view_for_existing_item.is_none()
-                    && item
-                        .project_path(cx)
-                        .map_or(false, |item_project_path| item_project_path == project_path)
-                {
-                    view_for_existing_item =
-                        Some(item.add_view(cx.window_id(), settings.clone(), cx.as_mut()));
-                }
-                true
-            } else {
-                false
-            }
-        });
-        if let Some(view) = view_for_existing_item {
-            pane.add_item_view(view.boxed_clone(), cx.as_mut());
-            Some(view)
-        } else {
-            None
-        }
+    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ModelHandle<T>> {
+        self.items
+            .iter()
+            .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
     }
 
     pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
@@ -791,24 +801,16 @@ impl Workspace {
                                     {
                                         error!("failed to save item: {:?}, ", error);
                                     }
-
-                                    handle.update(&mut cx, |this, cx| {
-                                        this.project.update(cx, |project, cx| project.diagnose(cx))
-                                    });
                                 })
                                 .detach();
                             }
                         },
                     );
                 } else {
-                    cx.spawn(|this, mut cx| async move {
+                    cx.spawn(|_, mut cx| async move {
                         if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
                             error!("failed to save item: {:?}, ", error);
                         }
-
-                        this.update(&mut cx, |this, cx| {
-                            this.project.update(cx, |project, cx| project.diagnose(cx))
-                        });
                     })
                     .detach();
                 }
@@ -840,10 +842,6 @@ impl Workspace {
                             if let Err(error) = result {
                                 error!("failed to save item: {:?}, ", error);
                             }
-
-                            handle.update(&mut cx, |this, cx| {
-                                this.project.update(cx, |project, cx| project.diagnose(cx))
-                            });
                         })
                         .detach()
                     }
@@ -920,19 +918,65 @@ impl Workspace {
         pane
     }
 
-    pub fn add_item<T>(
+    pub fn open_item<T>(
         &mut self,
         item_handle: T,
         cx: &mut ViewContext<Self>,
     ) -> Box<dyn ItemViewHandle>
     where
-        T: ItemHandle,
+        T: 'static + ItemHandle,
     {
-        let view = item_handle.add_view(cx.window_id(), self.settings.clone(), cx);
-        self.items.push(item_handle.downgrade());
-        self.active_pane()
-            .add_item_view(view.boxed_clone(), cx.as_mut());
-        view
+        self.open_item_in_pane(item_handle, &self.active_pane().clone(), cx)
+    }
+
+    pub fn open_item_in_pane<T>(
+        &mut self,
+        item_handle: T,
+        pane: &ViewHandle<Pane>,
+        cx: &mut ViewContext<Self>,
+    ) -> Box<dyn ItemViewHandle>
+    where
+        T: 'static + ItemHandle,
+    {
+        self.items.insert(item_handle.downgrade());
+        pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx))
+    }
+
+    pub fn activate_pane_for_item(
+        &mut self,
+        item: &dyn ItemHandle,
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        let pane = self.panes.iter().find_map(|pane| {
+            if pane.read(cx).contains_item(item) {
+                Some(pane.clone())
+            } else {
+                None
+            }
+        });
+        if let Some(pane) = pane {
+            self.activate_pane(pane.clone(), cx);
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
+        let result = self.panes.iter().find_map(|pane| {
+            if let Some(ix) = pane.read(cx).index_for_item(item) {
+                Some((pane.clone(), ix))
+            } else {
+                None
+            }
+        });
+        if let Some((pane, ix)) = result {
+            self.activate_pane(pane.clone(), cx);
+            pane.update(cx, |pane, cx| pane.activate_item(ix, cx));
+            true
+        } else {
+            false
+        }
     }
 
     fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
@@ -977,7 +1021,7 @@ impl Workspace {
         self.activate_pane(new_pane.clone(), cx);
         if let Some(item) = pane.read(cx).active_item() {
             if let Some(clone) = item.clone_on_split(cx.as_mut()) {
-                new_pane.add_item_view(clone, cx.as_mut());
+                new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx));
             }
         }
         self.center
@@ -1203,50 +1247,40 @@ impl View for Workspace {
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let settings = self.settings.borrow();
         let theme = &settings.theme;
-        Container::new(
-            Flex::column()
-                .with_child(self.render_titlebar(&theme, cx))
-                .with_child(
-                    Expanded::new(
-                        1.0,
-                        Stack::new()
-                            .with_child({
-                                let mut content = Flex::row();
-                                content.add_child(self.left_sidebar.render(&settings, cx));
-                                if let Some(element) =
-                                    self.left_sidebar.render_active_item(&settings, cx)
-                                {
-                                    content.add_child(Flexible::new(0.8, element).boxed());
-                                }
-                                content.add_child(
-                                    Flex::column()
-                                        .with_child(
-                                            Expanded::new(1.0, self.center.render(&settings.theme))
-                                                .boxed(),
-                                        )
-                                        .with_child(ChildView::new(self.status_bar.id()).boxed())
-                                        .expanded(1.)
+        Flex::column()
+            .with_child(self.render_titlebar(&theme, cx))
+            .with_child(
+                Stack::new()
+                    .with_child({
+                        let mut content = Flex::row();
+                        content.add_child(self.left_sidebar.render(&settings, cx));
+                        if let Some(element) = self.left_sidebar.render_active_item(&settings, cx) {
+                            content.add_child(Flexible::new(0.8, false, element).boxed());
+                        }
+                        content.add_child(
+                            Flex::column()
+                                .with_child(
+                                    Flexible::new(1., true, self.center.render(&settings.theme))
                                         .boxed(),
-                                );
-                                if let Some(element) =
-                                    self.right_sidebar.render_active_item(&settings, cx)
-                                {
-                                    content.add_child(Flexible::new(0.8, element).boxed());
-                                }
-                                content.add_child(self.right_sidebar.render(&settings, cx));
-                                content.boxed()
-                            })
-                            .with_children(
-                                self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()),
-                            )
-                            .boxed(),
-                    )
+                                )
+                                .with_child(ChildView::new(self.status_bar.id()).boxed())
+                                .flexible(1., true)
+                                .boxed(),
+                        );
+                        if let Some(element) = self.right_sidebar.render_active_item(&settings, cx)
+                        {
+                            content.add_child(Flexible::new(0.8, false, element).boxed());
+                        }
+                        content.add_child(self.right_sidebar.render(&settings, cx));
+                        content.boxed()
+                    })
+                    .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
+                    .flexible(1.0, true)
                     .boxed(),
-                )
-                .boxed(),
-        )
-        .with_background_color(settings.theme.workspace.background)
-        .named("workspace")
+            )
+            .contained()
+            .with_background_color(settings.theme.workspace.background)
+            .named("workspace")
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {

crates/zed/assets/themes/_base.toml 🔗

@@ -249,11 +249,28 @@ line_number_active = "$text.0.color"
 selection = "$selection.host"
 guest_selections = "$selection.guests"
 error_color = "$status.bad"
-error_diagnostic = { text = "$status.bad" }
 invalid_error_diagnostic = { text = "$text.3.color" }
-warning_diagnostic = { text = "$status.warn" }
 invalid_warning_diagnostic = { text = "$text.3.color" }
-information_diagnostic = { text = "$status.info" }
 invalid_information_diagnostic = { text = "$text.3.color" }
-hint_diagnostic = { text = "$status.info" }
 invalid_hint_diagnostic = { text = "$text.3.color" }
+
+[editor.error_diagnostic]
+text = "$status.bad"
+header = { padding = { left = 10 }, background = "#ffffff08" }
+
+[editor.warning_diagnostic]
+text = "$status.warn"
+header = { padding = { left = 10 }, background = "#ffffff08" }
+
+[editor.information_diagnostic]
+text = "$status.info"
+header = { padding = { left = 10 }, background = "#ffffff08" }
+
+[editor.hint_diagnostic]
+text = "$status.info"
+header = { padding = { left = 10 }, background = "#ffffff08" }
+
+[project_diagnostics]
+background = "$surface.1"
+empty_message = "$text.0"
+status_bar_item = { extends = "$text.2", margin.right = 10 }

crates/zed/languages/rust/config.toml 🔗

@@ -13,3 +13,4 @@ brackets = [
 [language_server]
 binary = "rust-analyzer"
 disk_based_diagnostic_sources = ["rustc"]
+disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check"

crates/zed/src/language.rs 🔗

@@ -7,184 +7,6 @@ use std::{str, sync::Arc};
 #[folder = "languages"]
 struct LanguageDir;
 
-mod rust {
-    use anyhow::Result;
-    use async_trait::async_trait;
-    use collections::{HashMap, HashSet};
-    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity};
-    use parking_lot::Mutex;
-    use serde::Deserialize;
-    use serde_json::Deserializer;
-    use smol::process::Command;
-    use std::path::{Path, PathBuf};
-    use std::sync::Arc;
-
-    #[derive(Default)]
-    pub struct DiagnosticProvider {
-        reported_paths: Mutex<HashSet<Arc<Path>>>,
-    }
-
-    #[derive(Debug, Deserialize)]
-    struct Check {
-        message: CompilerMessage,
-    }
-
-    #[derive(Debug, Deserialize)]
-    struct CompilerMessage {
-        code: Option<ErrorCode>,
-        spans: Vec<Span>,
-        message: String,
-        level: ErrorLevel,
-        children: Vec<CompilerMessage>,
-    }
-
-    #[derive(Debug, Deserialize)]
-    enum ErrorLevel {
-        #[serde(rename = "warning")]
-        Warning,
-        #[serde(rename = "error")]
-        Error,
-        #[serde(rename = "help")]
-        Help,
-        #[serde(rename = "note")]
-        Note,
-    }
-
-    #[derive(Debug, Deserialize)]
-    struct ErrorCode {
-        code: String,
-    }
-
-    #[derive(Clone, Debug, Deserialize)]
-    struct Span {
-        is_primary: bool,
-        file_name: PathBuf,
-        byte_start: usize,
-        byte_end: usize,
-        expansion: Option<Box<Expansion>>,
-    }
-
-    #[derive(Clone, Debug, Deserialize)]
-    struct Expansion {
-        span: Span,
-    }
-
-    #[async_trait]
-    impl language::DiagnosticProvider for DiagnosticProvider {
-        async fn diagnose(
-            &self,
-            root_path: Arc<Path>,
-        ) -> Result<HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>> {
-            let output = Command::new("cargo")
-                .arg("check")
-                .args(["--message-format", "json"])
-                .current_dir(&root_path)
-                .output()
-                .await?;
-
-            let mut group_id = 0;
-            let mut diagnostics_by_path = HashMap::default();
-            let mut new_reported_paths = HashSet::default();
-            for value in
-                Deserializer::from_slice(&output.stdout).into_iter::<&serde_json::value::RawValue>()
-            {
-                if let Ok(check) = serde_json::from_str::<Check>(value?.get()) {
-                    let check_severity = match check.message.level {
-                        ErrorLevel::Warning => DiagnosticSeverity::WARNING,
-                        ErrorLevel::Error => DiagnosticSeverity::ERROR,
-                        ErrorLevel::Help => DiagnosticSeverity::HINT,
-                        ErrorLevel::Note => DiagnosticSeverity::INFORMATION,
-                    };
-
-                    let mut primary_span = None;
-                    for mut span in check.message.spans {
-                        if let Some(mut expansion) = span.expansion {
-                            expansion.span.is_primary = span.is_primary;
-                            span = expansion.span;
-                        }
-
-                        let span_path: Arc<Path> = span.file_name.as_path().into();
-                        new_reported_paths.insert(span_path.clone());
-                        diagnostics_by_path
-                            .entry(span_path)
-                            .or_insert(Vec::new())
-                            .push(DiagnosticEntry {
-                                range: span.byte_start..span.byte_end,
-                                diagnostic: Diagnostic {
-                                    code: check.message.code.as_ref().map(|c| c.code.clone()),
-                                    severity: check_severity,
-                                    message: check.message.message.clone(),
-                                    group_id,
-                                    is_valid: true,
-                                    is_primary: span.is_primary,
-                                    is_disk_based: true,
-                                },
-                            });
-
-                        if span.is_primary {
-                            primary_span = Some(span);
-                        }
-                    }
-
-                    for mut child in check.message.children {
-                        if child.spans.is_empty() {
-                            if let Some(primary_span) = primary_span.clone() {
-                                child.spans.push(primary_span);
-                            }
-                        } else {
-                            // TODO
-                            continue;
-                        }
-
-                        let child_severity = match child.level {
-                            ErrorLevel::Warning => DiagnosticSeverity::WARNING,
-                            ErrorLevel::Error => DiagnosticSeverity::ERROR,
-                            ErrorLevel::Help => DiagnosticSeverity::HINT,
-                            ErrorLevel::Note => DiagnosticSeverity::INFORMATION,
-                        };
-
-                        for mut span in child.spans {
-                            if let Some(expansion) = span.expansion {
-                                span = expansion.span;
-                            }
-
-                            let span_path: Arc<Path> = span.file_name.as_path().into();
-                            new_reported_paths.insert(span_path.clone());
-                            diagnostics_by_path
-                                .entry(span_path)
-                                .or_insert(Vec::new())
-                                .push(DiagnosticEntry {
-                                    range: span.byte_start..span.byte_end,
-                                    diagnostic: Diagnostic {
-                                        code: child.code.as_ref().map(|c| c.code.clone()),
-                                        severity: child_severity,
-                                        message: child.message.clone(),
-                                        group_id,
-                                        is_valid: true,
-                                        is_primary: false,
-                                        is_disk_based: true,
-                                    },
-                                });
-                        }
-                    }
-
-                    group_id += 1;
-                }
-            }
-
-            let reported_paths = &mut *self.reported_paths.lock();
-            for old_reported_path in reported_paths.iter() {
-                if !diagnostics_by_path.contains_key(old_reported_path) {
-                    diagnostics_by_path.insert(old_reported_path.clone(), Default::default());
-                }
-            }
-            *reported_paths = new_reported_paths;
-
-            Ok(diagnostics_by_path)
-        }
-    }
-}
-
 pub fn build_language_registry() -> LanguageRegistry {
     let mut languages = LanguageRegistry::default();
     languages.add(Arc::new(rust()));
@@ -202,7 +24,6 @@ fn rust() -> Language {
         .unwrap()
         .with_indents_query(load_query("rust/indents.scm").as_ref())
         .unwrap()
-        .with_diagnostic_provider(rust::DiagnosticProvider::default())
 }
 
 fn markdown() -> Language {

crates/zed/src/main.rs 🔗

@@ -51,11 +51,11 @@ fn main() {
         let http = http::client();
         let client = client::Client::new(http.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
-        let mut entry_openers = Vec::new();
+        let mut path_openers = Vec::new();
 
         client::init(client.clone(), cx);
         workspace::init(cx);
-        editor::init(cx, &mut entry_openers);
+        editor::init(cx, &mut path_openers);
         go_to_line::init(cx);
         file_finder::init(cx);
         chat_panel::init(cx);
@@ -72,7 +72,7 @@ fn main() {
             client,
             user_store,
             fs: Arc::new(RealFs),
-            entry_openers: Arc::from(entry_openers),
+            path_openers: Arc::from(path_openers),
             build_window_options: &build_window_options,
             build_workspace: &build_workspace,
         });

crates/zed/src/test.rs 🔗

@@ -16,8 +16,8 @@ fn init_logger() {
 }
 
 pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
-    let mut entry_openers = Vec::new();
-    editor::init(cx, &mut entry_openers);
+    let mut path_openers = Vec::new();
+    editor::init(cx, &mut path_openers);
     let (settings_tx, settings) = watch::channel_with(build_settings(cx));
     let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
     let http = FakeHttpClient::with_404_response();
@@ -41,7 +41,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
         client,
         user_store,
         fs: Arc::new(FakeFs::new()),
-        entry_openers: Arc::from(entry_openers),
+        path_openers: Arc::from(path_openers),
         build_window_options: &build_window_options,
         build_workspace: &build_workspace,
     })

crates/zed/src/zed.rs 🔗

@@ -62,7 +62,7 @@ pub fn build_workspace(
         settings: app_state.settings.clone(),
         user_store: app_state.user_store.clone(),
         channel_list: app_state.channel_list.clone(),
-        entry_openers: app_state.entry_openers.clone(),
+        path_openers: app_state.path_openers.clone(),
     };
     let mut workspace = Workspace::new(&workspace_params, cx);
     let project = workspace.project().clone();
@@ -88,12 +88,20 @@ pub fn build_workspace(
         .into(),
     );
 
-    let diagnostic =
+    let diagnostic_message =
         cx.add_view(|_| editor::items::DiagnosticMessage::new(app_state.settings.clone()));
+    let diagnostic_summary = cx.add_view(|cx| {
+        diagnostics::items::DiagnosticSummary::new(
+            workspace.project(),
+            app_state.settings.clone(),
+            cx,
+        )
+    });
     let cursor_position =
         cx.add_view(|_| editor::items::CursorPosition::new(app_state.settings.clone()));
     workspace.status_bar().update(cx, |status_bar, cx| {
-        status_bar.add_left_item(diagnostic, cx);
+        status_bar.add_left_item(diagnostic_summary, cx);
+        status_bar.add_left_item(diagnostic_message, cx);
         status_bar.add_right_item(cursor_position, cx);
     });
 
@@ -256,8 +264,7 @@ mod tests {
 
         // Open the first entry
         let entry_1 = workspace
-            .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
-            .unwrap()
+            .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx))
             .await
             .unwrap();
         cx.read(|cx| {
@@ -266,13 +273,12 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file1.clone())
             );
-            assert_eq!(pane.items().len(), 1);
+            assert_eq!(pane.item_views().count(), 1);
         });
 
         // Open the second entry
         workspace
-            .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx))
-            .unwrap()
+            .update(&mut cx, |w, cx| w.open_path(file2.clone(), cx))
             .await
             .unwrap();
         cx.read(|cx| {
@@ -281,12 +287,12 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file2.clone())
             );
-            assert_eq!(pane.items().len(), 2);
+            assert_eq!(pane.item_views().count(), 2);
         });
 
         // Open the first entry again. The existing pane item is activated.
         let entry_1b = workspace
-            .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx).unwrap())
+            .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx))
             .await
             .unwrap();
         assert_eq!(entry_1.id(), entry_1b.id());
@@ -297,14 +303,14 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file1.clone())
             );
-            assert_eq!(pane.items().len(), 2);
+            assert_eq!(pane.item_views().count(), 2);
         });
 
         // Split the pane with the first entry, then open the second entry again.
         workspace
             .update(&mut cx, |w, cx| {
                 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
-                w.open_entry(file2.clone(), cx).unwrap()
+                w.open_path(file2.clone(), cx)
             })
             .await
             .unwrap();
@@ -323,8 +329,8 @@ mod tests {
         // Open the third entry twice concurrently. Only one pane item is added.
         let (t1, t2) = workspace.update(&mut cx, |w, cx| {
             (
-                w.open_entry(file3.clone(), cx).unwrap(),
-                w.open_entry(file3.clone(), cx).unwrap(),
+                w.open_path(file3.clone(), cx),
+                w.open_path(file3.clone(), cx),
             )
         });
         t1.await.unwrap();
@@ -336,8 +342,7 @@ mod tests {
                 Some(file3.clone())
             );
             let pane_entries = pane
-                .items()
-                .iter()
+                .item_views()
                 .map(|i| i.project_path(cx).unwrap())
                 .collect::<Vec<_>>();
             assert_eq!(pane_entries, &[file1, file2, file3]);
@@ -553,15 +558,13 @@ mod tests {
         workspace
             .update(&mut cx, |workspace, cx| {
                 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
-                workspace
-                    .open_entry(
-                        ProjectPath {
-                            worktree_id: worktree.read(cx).id(),
-                            path: Path::new("the-new-name.rs").into(),
-                        },
-                        cx,
-                    )
-                    .unwrap()
+                workspace.open_path(
+                    ProjectPath {
+                        worktree_id: worktree.read(cx).id(),
+                        path: Path::new("the-new-name.rs").into(),
+                    },
+                    cx,
+                )
             })
             .await
             .unwrap();
@@ -574,7 +577,10 @@ mod tests {
                 .unwrap()
         });
         cx.read(|cx| {
-            assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer());
+            assert_eq!(
+                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
+                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
+            );
         })
     }
 
@@ -658,8 +664,7 @@ mod tests {
         let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
 
         workspace
-            .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
-            .unwrap()
+            .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx))
             .await
             .unwrap();
         cx.read(|cx| {