WIP - Start work on updating project diagnostics view

Max Brunsfeld created

Change summary

Cargo.lock                            |   3 
crates/diagnostics/Cargo.toml         |   3 
crates/diagnostics/src/diagnostics.rs | 227 ++++++++++++++++++++++------
crates/project/src/project.rs         |  11 +
crates/project/src/worktree.rs        |  25 ++
5 files changed, 214 insertions(+), 55 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1404,13 +1404,16 @@ name = "diagnostics"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "client",
  "collections",
  "editor",
  "gpui",
  "language",
  "postage",
  "project",
+ "serde_json",
  "unindent",
+ "util",
  "workspace",
 ]
 

crates/diagnostics/Cargo.toml 🔗

@@ -13,12 +13,15 @@ editor = { path = "../editor" }
 language = { path = "../language" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+util = { path = "../util" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4", features = ["futures-traits"] }
 
 [dev-dependencies]
 unindent = "0.1"
+client = { path = "../client", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
+serde_json = { version = "1", features = ["preserve_order"] }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -12,6 +12,7 @@ use language::{Bias, Buffer, Point};
 use postage::watch;
 use project::Project;
 use std::ops::Range;
+use util::TryFutureExt;
 use workspace::Workspace;
 
 action!(Toggle);
@@ -65,11 +66,49 @@ impl View for ProjectDiagnosticsEditor {
 
 impl ProjectDiagnosticsEditor {
     fn new(
-        replica_id: u16,
+        project: ModelHandle<Project>,
         settings: watch::Receiver<workspace::Settings>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let excerpts = cx.add_model(|_| MultiBuffer::new(replica_id));
+        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))
+                }
+                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();
+            }
+        })
+        .detach();
+
+        let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
         let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
         let editor =
             cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
@@ -82,6 +121,11 @@ impl ProjectDiagnosticsEditor {
         }
     }
 
+    #[cfg(test)]
+    fn text(&self, cx: &AppContext) -> String {
+        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);
@@ -193,6 +237,7 @@ impl ProjectDiagnosticsEditor {
                 cx,
             );
         });
+        cx.notify();
     }
 }
 
@@ -205,27 +250,7 @@ impl workspace::Item for ProjectDiagnostics {
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View {
         let project = handle.read(cx).project.clone();
-        let project_paths = project
-            .read(cx)
-            .diagnostic_summaries(cx)
-            .map(|e| e.0)
-            .collect::<Vec<_>>();
-
-        cx.spawn(|view, 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?;
-                    view.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
-                }
-                Result::<_, anyhow::Error>::Ok(())
-            }
-        })
-        .detach();
-
-        ProjectDiagnosticsEditor::new(project.read(cx).replica_id(), settings, cx)
+        ProjectDiagnosticsEditor::new(project, settings, cx)
     }
 
     fn project_path(&self) -> Option<project::ProjectPath> {
@@ -282,35 +307,68 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
+    use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
+    use gpui::TestAppContext;
+    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
+    use project::FakeFs;
+    use serde_json::json;
+    use std::sync::Arc;
     use unindent::Unindent as _;
     use workspace::WorkspaceParams;
 
     #[gpui::test]
-    fn test_diagnostics(cx: &mut MutableAppContext) {
-        let settings = WorkspaceParams::test(cx).settings;
-        let view = cx.add_view(Default::default(), |cx| {
-            ProjectDiagnosticsEditor::new(0, settings, cx)
+    async fn test_diagnostics(mut cx: TestAppContext) {
+        let settings = cx.update(WorkspaceParams::test).settings;
+        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
+        let client = Client::new();
+        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+        let fs = Arc::new(FakeFs::new());
+
+        let project = cx.update(|cx| {
+            Project::local(
+                client.clone(),
+                user_store,
+                Arc::new(LanguageRegistry::new()),
+                fs.clone(),
+                cx,
+            )
         });
 
-        let text = "
-        fn main() {
-            let x = vec![];
-            let y = vec![];
-            a(x);
-            b(y);
-            // comment 1
-            // comment 2
-            c(y);
-            d(x);
-        }
-        "
-        .unindent();
-
-        let buffer = cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, text, cx);
-            buffer
-                .update_diagnostics(
+        fs.insert_tree(
+            "/test",
+            json!({
+                "a.rs": "
+                    const a: i32 = 'a';
+                ".unindent(),
+
+                "main.rs": "
+                    fn main() {
+                        let x = vec![];
+                        let y = vec![];
+                        a(x);
+                        b(y);
+                        // comment 1
+                        // comment 2
+                        c(y);
+                        d(x);
+                    }
+                "
+                .unindent(),
+            }),
+        )
+        .await;
+
+        let worktree = project
+            .update(&mut cx, |project, cx| {
+                project.add_local_worktree("/test", cx)
+            })
+            .await
+            .unwrap();
+
+        worktree.update(&mut cx, |worktree, cx| {
+            worktree
+                .update_diagnostic_entries(
+                    Arc::from("/test/main.rs".as_ref()),
                     None,
                     vec![
                         DiagnosticEntry {
@@ -381,11 +439,16 @@ mod tests {
                     cx,
                 )
                 .unwrap();
-            buffer
         });
 
-        view.update(cx, |view, cx| {
-            view.populate_excerpts(buffer, cx);
+        let view = cx.add_view(Default::default(), |cx| {
+            ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
+        });
+
+        view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
+            .await;
+
+        view.update(&mut cx, |view, cx| {
             let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 
             assert_eq!(
@@ -423,5 +486,71 @@ mod tests {
                 )
             );
         });
+
+        worktree.update(&mut cx, |worktree, cx| {
+            worktree
+                .update_diagnostic_entries(
+                    Arc::from("/test/a.rs".as_ref()),
+                    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,
+                            group_id: 0,
+                            ..Default::default()
+                        },
+                    }],
+                    cx,
+                )
+                .unwrap();
+        });
+
+        view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
+            .await;
+
+        view.update(&mut cx, |view, cx| {
+            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
+
+            assert_eq!(
+                editor.text(),
+                concat!(
+                    // a.rs
+                    "\n", // primary message
+                    "\n", // filename
+                    "const a: i32 = 'a';\n",
+                    // main.rs, diagnostic group 1
+                    "\n", // primary message
+                    "\n", // filename
+                    "    let x = vec![];\n",
+                    "    let y = vec![];\n",
+                    "\n", // supporting diagnostic
+                    "    a(x);\n",
+                    "    b(y);\n",
+                    "\n", // supporting diagnostic
+                    "    // comment 1\n",
+                    "    // comment 2\n",
+                    "    c(y);\n",
+                    "\n", // supporting diagnostic
+                    "    d(x);\n",
+                    // main.rs, diagnostic group 2
+                    "\n", // primary message
+                    "\n", // filename
+                    "fn main() {\n",
+                    "    let x = vec![];\n",
+                    "\n", // supporting diagnostic
+                    "    let y = vec![];\n",
+                    "    a(x);\n",
+                    "\n", // supporting diagnostic
+                    "    b(y);\n",
+                    "\n", // context ellipsis
+                    "    c(y);\n",
+                    "    d(x);\n",
+                    "\n", // supporting diagnostic
+                    "}"
+                )
+            );
+        });
     }
 }

crates/project/src/project.rs 🔗

@@ -56,9 +56,11 @@ pub struct Collaborator {
     pub replica_id: ReplicaId,
 }
 
+#[derive(Debug)]
 pub enum Event {
     ActiveEntryChanged(Option<ProjectEntry>),
     WorktreeRemoved(usize),
+    DiagnosticsUpdated(ProjectPath),
 }
 
 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
@@ -473,6 +475,15 @@ 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 {
+            worktree::Event::DiagnosticsUpdated(path) => {
+                cx.emit(Event::DiagnosticsUpdated(ProjectPath {
+                    worktree_id: worktree.id(),
+                    path: path.clone(),
+                }));
+            }
+        })
+        .detach();
         self.worktrees.push(worktree);
         cx.notify();
     }

crates/project/src/worktree.rs 🔗

@@ -64,8 +64,9 @@ pub enum Worktree {
     Remote(RemoteWorktree),
 }
 
+#[derive(Debug)]
 pub enum Event {
-    Closed,
+    DiagnosticsUpdated(Arc<Path>),
 }
 
 impl Entity for Worktree {
@@ -671,7 +672,7 @@ impl Worktree {
         }
     }
 
-    fn update_diagnostics(
+    pub fn update_diagnostics(
         &mut self,
         mut params: lsp::PublishDiagnosticsParams,
         cx: &mut ModelContext<Worktree>,
@@ -736,17 +737,28 @@ impl Worktree {
             })
             .collect::<Vec<_>>();
 
+        self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)
+    }
+
+    pub fn update_diagnostic_entries(
+        &mut self,
+        path: Arc<Path>,
+        version: Option<i32>,
+        diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Result<()> {
+        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)
+                    .map_or(false, |file| *file.path() == path)
                 {
                     let (remote_id, operation) = buffer.update(cx, |buffer, cx| {
                         (
                             buffer.remote_id(),
-                            buffer.update_diagnostics(params.version, diagnostics.clone(), cx),
+                            buffer.update_diagnostics(version, diagnostics.clone(), cx),
                         )
                     });
                     self.send_buffer_update(remote_id, operation?, cx);
@@ -757,8 +769,9 @@ impl Worktree {
 
         let this = self.as_local_mut().unwrap();
         this.diagnostic_summaries
-            .insert(worktree_path.clone(), DiagnosticSummary::new(&diagnostics));
-        this.diagnostics.insert(worktree_path.clone(), diagnostics);
+            .insert(path.clone(), DiagnosticSummary::new(&diagnostics));
+        this.diagnostics.insert(path.clone(), diagnostics);
+        cx.emit(Event::DiagnosticsUpdated(path.clone()));
         Ok(())
     }