Allow dumping the project diagnostic view's state as JSON

Max Brunsfeld and Nathan Sobo created

Also, improve DebugElements action so that it shows the JSON in an editor.

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

Change summary

assets/keymaps/default.json           |  2 
crates/diagnostics/src/diagnostics.rs | 31 +++++++++++++++++++++++++++-
crates/diagnostics/src/items.rs       |  7 +++++
crates/editor/src/editor.rs           |  2 
crates/gpui/src/app.rs                | 20 ++++++++++++++++++
crates/gpui/src/presenter.rs          | 20 +++++++++++-------
crates/project/src/project.rs         | 24 +++++++++++++++------
crates/workspace/src/workspace.rs     | 26 +++--------------------
crates/zed/src/zed.rs                 | 26 ++++++++++++++++++++++++
9 files changed, 116 insertions(+), 42 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -2,7 +2,7 @@
     "*": {
         "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
         "cmd-s": "workspace::Save",
-        "cmd-alt-i": "workspace::DebugElements",
+        "cmd-alt-i": "zed::DebugElements",
         "cmd-k cmd-left": "workspace::ActivatePreviousPane",
         "cmd-k cmd-right": "workspace::ActivateNextPane",
         "cmd-=": "zed::IncreaseBufferFontSize",

crates/diagnostics/src/diagnostics.rs 🔗

@@ -8,13 +8,15 @@ use editor::{
     highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
 };
 use gpui::{
-    actions, elements::*, fonts::TextStyle, AnyViewHandle, AppContext, Entity, ModelHandle,
-    MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
+    ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
+    WeakViewHandle,
 };
 use language::{
     Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
 };
 use project::{DiagnosticSummary, Project, ProjectPath};
+use serde_json::json;
 use settings::Settings;
 use std::{
     any::{Any, TypeId},
@@ -90,6 +92,31 @@ impl View for ProjectDiagnosticsEditor {
             cx.focus(&self.editor);
         }
     }
+
+    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+        let project = self.project.read(cx);
+        json!({
+            "project": json!({
+                "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
+                "summary": project.diagnostic_summary(cx),
+            }),
+            "summary": self.summary,
+            "paths_to_update": self.paths_to_update.iter().map(|path|
+                path.path.to_string_lossy()
+            ).collect::<Vec<_>>(),
+            "paths_states": self.path_states.iter().map(|state|
+                json!({
+                    "path": state.path.path.to_string_lossy(),
+                    "groups": state.diagnostic_groups.iter().map(|group|
+                        json!({
+                            "block_count": group.blocks.len(),
+                            "excerpt_count": group.excerpts.len(),
+                        })
+                    ).collect::<Vec<_>>(),
+                })
+            ).collect::<Vec<_>>(),
+        })
+    }
 }
 
 impl ProjectDiagnosticsEditor {

crates/diagnostics/src/items.rs 🔗

@@ -1,6 +1,7 @@
 use crate::render_summary;
 use gpui::{
-    elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
+    elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, RenderContext, View,
+    ViewContext,
 };
 use project::Project;
 use settings::Settings;
@@ -67,6 +68,10 @@ impl View for DiagnosticSummary {
         .on_click(|cx| cx.dispatch_action(crate::Deploy))
         .boxed()
     }
+
+    fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
+        serde_json::json!({ "summary": self.summary })
+    }
 }
 
 impl StatusItemView for DiagnosticSummary {

crates/editor/src/editor.rs 🔗

@@ -1008,7 +1008,7 @@ impl Editor {
         if project.read(cx).is_remote() {
             cx.propagate_action();
         } else if let Some(buffer) = project
-            .update(cx, |project, cx| project.create_buffer(cx))
+            .update(cx, |project, cx| project.create_buffer("", None, cx))
             .log_err()
         {
             workspace.add_item(

crates/gpui/src/app.rs 🔗

@@ -62,6 +62,9 @@ pub trait View: Entity + Sized {
         cx.set.insert(Self::ui_name().into());
         cx
     }
+    fn debug_json(&self, _: &AppContext) -> serde_json::Value {
+        serde_json::Value::Null
+    }
 }
 
 pub trait ReadModel {
@@ -2277,6 +2280,12 @@ pub struct AppContext {
 }
 
 impl AppContext {
+    pub(crate) fn root_view(&self, window_id: usize) -> Option<AnyViewHandle> {
+        self.windows
+            .get(&window_id)
+            .map(|window| window.root_view.clone())
+    }
+
     pub fn root_view_id(&self, window_id: usize) -> Option<usize> {
         self.windows
             .get(&window_id)
@@ -2590,6 +2599,7 @@ pub trait AnyView {
     fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
     fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
     fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
+    fn debug_json(&self, cx: &AppContext) -> serde_json::Value;
 }
 
 impl<T> AnyView for T
@@ -2653,6 +2663,10 @@ where
     fn keymap_context(&self, cx: &AppContext) -> keymap::Context {
         View::keymap_context(self, cx)
     }
+
+    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+        View::debug_json(self, cx)
+    }
 }
 
 pub struct ModelContext<'a, T: ?Sized> {
@@ -3927,6 +3941,12 @@ impl AnyViewHandle {
     pub fn view_type(&self) -> TypeId {
         self.view_type
     }
+
+    pub fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+        cx.views
+            .get(&(self.window_id, self.view_id))
+            .map_or_else(|| serde_json::Value::Null, |view| view.debug_json(cx))
+    }
 }
 
 impl Clone for AnyViewHandle {

crates/gpui/src/presenter.rs 🔗

@@ -209,15 +209,18 @@ impl Presenter {
     }
 
     pub fn debug_elements(&self, cx: &AppContext) -> Option<json::Value> {
-        cx.root_view_id(self.window_id)
-            .and_then(|root_view_id| self.rendered_views.get(&root_view_id))
-            .map(|root_element| {
-                root_element.debug(&DebugContext {
-                    rendered_views: &self.rendered_views,
-                    font_cache: &self.font_cache,
-                    app: cx,
+        let view = cx.root_view(self.window_id)?;
+        Some(json!({
+            "root_view": view.debug_json(cx),
+            "root_element": self.rendered_views.get(&view.id())
+                .map(|root_element| {
+                    root_element.debug(&DebugContext {
+                        rendered_views: &self.rendered_views,
+                        font_cache: &self.font_cache,
+                        app: cx,
+                    })
                 })
-            })
+        }))
     }
 }
 
@@ -554,6 +557,7 @@ impl Element for ChildView {
             "type": "ChildView",
             "view_id": self.view.id(),
             "bounds": bounds.to_json(),
+            "view": self.view.debug_json(cx.app),
             "child": if let Some(view) = cx.rendered_views.get(&self.view.id()) {
                 view.debug(cx)
             } else {

crates/project/src/project.rs 🔗

@@ -28,6 +28,7 @@ use parking_lot::Mutex;
 use postage::watch;
 use rand::prelude::*;
 use search::SearchQuery;
+use serde::Serialize;
 use settings::Settings;
 use sha2::{Digest, Sha256};
 use similar::{ChangeTag, TextDiff};
@@ -132,16 +133,18 @@ pub enum Event {
     CollaboratorLeft(PeerId),
 }
 
+#[derive(Serialize)]
 pub struct LanguageServerStatus {
     pub name: String,
     pub pending_work: BTreeMap<String, LanguageServerProgress>,
-    pending_diagnostic_updates: isize,
+    pub pending_diagnostic_updates: isize,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize)]
 pub struct LanguageServerProgress {
     pub message: Option<String>,
     pub percentage: Option<usize>,
+    #[serde(skip_serializing)]
     pub last_update_at: Instant,
 }
 
@@ -151,7 +154,7 @@ pub struct ProjectPath {
     pub path: Arc<Path>,
 }
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, PartialEq, Serialize)]
 pub struct DiagnosticSummary {
     pub error_count: usize,
     pub warning_count: usize,
@@ -467,7 +470,6 @@ impl Project {
             .and_then(|buffer| buffer.upgrade(cx))
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn languages(&self) -> &Arc<LanguageRegistry> {
         &self.languages
     }
@@ -813,13 +815,19 @@ impl Project {
         !self.is_local()
     }
 
-    pub fn create_buffer(&mut self, cx: &mut ModelContext<Self>) -> Result<ModelHandle<Buffer>> {
+    pub fn create_buffer(
+        &mut self,
+        text: &str,
+        language: Option<Arc<Language>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<ModelHandle<Buffer>> {
         if self.is_remote() {
             return Err(anyhow!("creating buffers as a guest is not supported yet"));
         }
 
         let buffer = cx.add_model(|cx| {
-            Buffer::new(self.replica_id(), "", cx).with_language(language::PLAIN_TEXT.clone(), cx)
+            Buffer::new(self.replica_id(), text, cx)
+                .with_language(language.unwrap_or(language::PLAIN_TEXT.clone()), cx)
         });
         self.register_buffer(&buffer, cx)?;
         Ok(buffer)
@@ -6581,7 +6589,9 @@ mod tests {
             .unwrap();
         let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
 
-        let buffer = project.update(cx, |project, cx| project.create_buffer(cx).unwrap());
+        let buffer = project.update(cx, |project, cx| {
+            project.create_buffer("", None, cx).unwrap()
+        });
         buffer.update(cx, |buffer, cx| {
             buffer.edit([0..0], "abc", cx);
             assert!(buffer.is_dirty());

crates/workspace/src/workspace.rs 🔗

@@ -18,11 +18,11 @@ use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
     impl_internal_actions,
-    json::{self, to_string_pretty, ToJson},
+    json::{self, ToJson},
     platform::{CursorStyle, WindowOptions},
-    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity,
-    ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task,
-    View, ViewContext, ViewHandle, WeakViewHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
+    ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
 use log::error;
@@ -75,7 +75,6 @@ actions!(
         ToggleShare,
         Unfollow,
         Save,
-        DebugElements,
         ActivatePreviousPane,
         ActivateNextPane,
         FollowNextCollaborator,
@@ -133,7 +132,6 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
             workspace.save_active_item(cx).detach_and_log_err(cx);
         },
     );
-    cx.add_action(Workspace::debug_elements);
     cx.add_action(Workspace::toggle_sidebar_item);
     cx.add_action(Workspace::toggle_sidebar_item_focus);
     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
@@ -1053,22 +1051,6 @@ impl Workspace {
         cx.notify();
     }
 
-    pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
-        match to_string_pretty(&cx.debug_elements()) {
-            Ok(json) => {
-                let kib = json.len() as f32 / 1024.;
-                cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
-                log::info!(
-                    "copied {:.1} KiB of element debug JSON to the clipboard",
-                    kib
-                );
-            }
-            Err(error) => {
-                log::error!("error debugging elements: {}", error);
-            }
-        };
-    }
-
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
         let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();

crates/zed/src/zed.rs 🔗

@@ -10,6 +10,7 @@ pub use client;
 pub use contacts_panel;
 use contacts_panel::ContactsPanel;
 pub use editor;
+use editor::Editor;
 use gpui::{
     actions,
     geometry::vector::vec2f,
@@ -22,8 +23,10 @@ use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
+use serde_json::to_string_pretty;
 use settings::Settings;
 use std::{path::PathBuf, sync::Arc};
+use util::ResultExt;
 pub use workspace;
 use workspace::{AppState, Workspace, WorkspaceParams};
 
@@ -32,6 +35,7 @@ actions!(
     [
         About,
         Quit,
+        DebugElements,
         OpenSettings,
         IncreaseBufferFontSize,
         DecreaseBufferFontSize
@@ -100,6 +104,28 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             .detach_and_log_err(cx);
         }
     });
+    cx.add_action(
+        |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
+            let content = to_string_pretty(&cx.debug_elements()).unwrap();
+            let project = workspace.project().clone();
+            let json_language = project.read(cx).languages().get_language("JSON").unwrap();
+            if project.read(cx).is_remote() {
+                cx.propagate_action();
+            } else if let Some(buffer) = project
+                .update(cx, |project, cx| {
+                    project.create_buffer(&content, Some(json_language), cx)
+                })
+                .log_err()
+            {
+                workspace.add_item(
+                    Box::new(
+                        cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
+                    ),
+                    cx,
+                );
+            }
+        },
+    );
 
     workspace::lsp_status::init(cx);