Merge branch 'project-diagnostics-pinned-tab' into style-project-diagnostics

Max Brunsfeld created

Change summary

Cargo.lock                            |  1 
crates/diagnostics/Cargo.toml         |  1 
crates/diagnostics/src/diagnostics.rs | 51 +++++++++++++++++++++------
crates/editor/src/editor.rs           | 19 ++++++++-
crates/editor/src/items.rs            | 15 ++------
crates/file_finder/src/file_finder.rs | 10 ++++
crates/theme/src/theme.rs             |  3 +
crates/workspace/src/pane.rs          | 53 ++++++++++------------------
crates/workspace/src/workspace.rs     |  8 ++--
crates/zed/assets/icons/no.svg        |  1 
crates/zed/assets/icons/warning.svg   |  4 +-
crates/zed/assets/themes/_base.toml   |  3 +
crates/zed/src/zed.rs                 | 16 ++++++--
13 files changed, 115 insertions(+), 70 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1412,6 +1412,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "theme",
  "unindent",
  "util",
  "workspace",

crates/diagnostics/Cargo.toml 🔗

@@ -13,6 +13,7 @@ editor = { path = "../editor" }
 language = { path = "../language" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4", features = ["futures-traits"] }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -17,7 +17,7 @@ use language::{
     Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
 };
 use postage::watch;
-use project::{Project, ProjectPath};
+use project::{DiagnosticSummary, Project, ProjectPath};
 use std::{
     any::{Any, TypeId},
     cmp::Ordering,
@@ -57,6 +57,7 @@ struct ProjectDiagnosticsEditor {
     model: ModelHandle<ProjectDiagnostics>,
     workspace: WeakViewHandle<Workspace>,
     editor: ViewHandle<Editor>,
+    summary: DiagnosticSummary,
     excerpts: ModelHandle<MultiBuffer>,
     path_states: Vec<PathState>,
     paths_to_update: BTreeSet<ProjectPath>,
@@ -132,6 +133,7 @@ impl ProjectDiagnosticsEditor {
             project::Event::DiskBasedDiagnosticsFinished => {
                 let paths = mem::take(&mut this.paths_to_update);
                 this.update_excerpts(paths, cx);
+                cx.emit(Event::TitleChanged)
             }
             project::Event::DiagnosticsUpdated(path) => {
                 this.paths_to_update.insert(path.clone());
@@ -147,13 +149,11 @@ impl ProjectDiagnosticsEditor {
         cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
             .detach();
 
-        let paths_to_update = project
-            .read(cx)
-            .diagnostic_summaries(cx)
-            .map(|e| e.0)
-            .collect();
+        let project = project.read(cx);
+        let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
         let this = Self {
             model,
+            summary: project.diagnostic_summary(cx),
             workspace,
             excerpts,
             editor,
@@ -556,8 +556,38 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         self.model.clone()
     }
 
-    fn title(&self, _: &AppContext) -> String {
-        "Project Diagnostics".to_string()
+    fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
+        let theme = &self.settings.borrow().theme.project_diagnostics;
+        let icon_width = theme.tab_icon_width;
+        let icon_spacing = theme.tab_icon_spacing;
+        let summary_spacing = theme.tab_summary_spacing;
+        Flex::row()
+            .with_children([
+                Svg::new("icons/no.svg")
+                    .with_color(style.label.text.color)
+                    .constrained()
+                    .with_width(icon_width)
+                    .aligned()
+                    .contained()
+                    .with_margin_right(icon_spacing)
+                    .named("no-icon"),
+                Label::new(self.summary.error_count.to_string(), style.label.clone())
+                    .aligned()
+                    .boxed(),
+                Svg::new("icons/warning.svg")
+                    .with_color(style.label.text.color)
+                    .constrained()
+                    .with_width(icon_width)
+                    .aligned()
+                    .contained()
+                    .with_margin_left(summary_spacing)
+                    .with_margin_right(icon_spacing)
+                    .named("warn-icon"),
+                Label::new(self.summary.warning_count.to_string(), style.label.clone())
+                    .aligned()
+                    .boxed(),
+            ])
+            .boxed()
     }
 
     fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
@@ -603,10 +633,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
     }
 
     fn should_update_tab_on_event(event: &Event) -> bool {
-        matches!(
-            event,
-            Event::Saved | Event::Dirtied | Event::FileHandleChanged
-        )
+        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
     }
 
     fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>

crates/editor/src/editor.rs 🔗

@@ -551,6 +551,19 @@ impl Editor {
         &self.buffer
     }
 
+    pub fn title(&self, cx: &AppContext) -> String {
+        let filename = self
+            .buffer()
+            .read(cx)
+            .file(cx)
+            .map(|file| file.file_name(cx));
+        if let Some(name) = filename {
+            name.to_string_lossy().into()
+        } else {
+            "untitled".into()
+        }
+    }
+
     pub fn snapshot(&mut self, cx: &mut MutableAppContext) -> EditorSnapshot {
         EditorSnapshot {
             mode: self.mode,
@@ -3762,8 +3775,8 @@ impl Editor {
             language::Event::Edited => cx.emit(Event::Edited),
             language::Event::Dirtied => cx.emit(Event::Dirtied),
             language::Event::Saved => cx.emit(Event::Saved),
-            language::Event::FileHandleChanged => cx.emit(Event::FileHandleChanged),
-            language::Event::Reloaded => cx.emit(Event::FileHandleChanged),
+            language::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
+            language::Event::Reloaded => cx.emit(Event::TitleChanged),
             language::Event::Closed => cx.emit(Event::Closed),
             _ => {}
         }
@@ -3903,7 +3916,7 @@ pub enum Event {
     Blurred,
     Dirtied,
     Saved,
-    FileHandleChanged,
+    TitleChanged,
     Closed,
 }
 

crates/editor/src/items.rs 🔗

@@ -121,13 +121,9 @@ impl ItemView for Editor {
         }
     }
 
-    fn title(&self, cx: &AppContext) -> String {
-        let file = self.buffer().read(cx).file(cx);
-        if let Some(file) = file {
-            file.file_name(cx).to_string_lossy().into()
-        } else {
-            "untitled".into()
-        }
+    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
+        let title = self.title(cx);
+        Label::new(title, style.label.clone()).boxed()
     }
 
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
@@ -207,10 +203,7 @@ impl ItemView for Editor {
     }
 
     fn should_update_tab_on_event(event: &Event) -> bool {
-        matches!(
-            event,
-            Event::Saved | Event::Dirtied | Event::FileHandleChanged
-        )
+        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
     }
 }
 

crates/file_finder/src/file_finder.rs 🔗

@@ -492,7 +492,15 @@ mod tests {
             .await;
         cx.read(|cx| {
             let active_item = active_pane.read(cx).active_item().unwrap();
-            assert_eq!(active_item.title(cx), "bandana");
+            assert_eq!(
+                active_item
+                    .to_any()
+                    .downcast::<Editor>()
+                    .unwrap()
+                    .read(cx)
+                    .title(cx),
+                "bandana"
+            );
         });
     }
 

crates/theme/src/theme.rs 🔗

@@ -235,6 +235,9 @@ pub struct ProjectDiagnostics {
     pub container: ContainerStyle,
     pub empty_message: TextStyle,
     pub status_bar_item: ContainedText,
+    pub tab_icon_width: f32,
+    pub tab_icon_spacing: f32,
+    pub tab_summary_spacing: f32,
 }
 
 #[derive(Clone, Deserialize, Default)]

crates/workspace/src/pane.rs 🔗

@@ -70,8 +70,6 @@ pub enum Event {
     Split(SplitDirection),
 }
 
-const MAX_TAB_TITLE_LEN: usize = 24;
-
 pub struct Pane {
     item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
     active_item_index: usize,
@@ -79,6 +77,11 @@ pub struct Pane {
     nav_history: Rc<RefCell<NavHistory>>,
 }
 
+// #[derive(Debug, Eq, PartialEq)]
+// pub struct State {
+//     pub tabs: Vec<TabState>,
+// }
+
 pub struct ItemNavHistory {
     history: Rc<RefCell<NavHistory>>,
     item_view: Rc<dyn WeakItemViewHandle>,
@@ -373,15 +376,12 @@ impl Pane {
                 let is_active = ix == self.active_item_index;
 
                 row.add_child({
-                    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) {
-                            truncated_len -= 1;
-                        }
-                        title.truncate(truncated_len);
-                        title.push('…');
-                    }
+                    let tab_style = if is_active {
+                        theme.workspace.active_tab.clone()
+                    } else {
+                        theme.workspace.tab.clone()
+                    };
+                    let title = item_view.tab_content(&tab_style, cx);
 
                     let mut style = if is_active {
                         theme.workspace.active_tab.clone()
@@ -430,29 +430,16 @@ impl Pane {
                                     .boxed(),
                                 )
                                 .with_child(
-                                    Container::new(
-                                        Align::new(
-                                            Label::new(
-                                                title,
-                                                if is_active {
-                                                    theme.workspace.active_tab.label.clone()
-                                                } else {
-                                                    theme.workspace.tab.label.clone()
-                                                },
-                                            )
-                                            .boxed(),
-                                        )
-                                        .boxed(),
-                                    )
-                                    .with_style(ContainerStyle {
-                                        margin: Margin {
-                                            left: style.spacing,
-                                            right: style.spacing,
+                                    Container::new(Align::new(title).boxed())
+                                        .with_style(ContainerStyle {
+                                            margin: Margin {
+                                                left: style.spacing,
+                                                right: style.spacing,
+                                                ..Default::default()
+                                            },
                                             ..Default::default()
-                                        },
-                                        ..Default::default()
-                                    })
-                                    .boxed(),
+                                        })
+                                        .boxed(),
                                 )
                                 .with_child(
                                     Align::new(

crates/workspace/src/workspace.rs 🔗

@@ -155,7 +155,7 @@ pub trait ItemView: View {
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
     fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle;
-    fn title(&self, cx: &AppContext) -> String;
+    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
     fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
     where
@@ -223,7 +223,7 @@ pub trait WeakItemHandle {
 
 pub trait ItemViewHandle: 'static {
     fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
-    fn title(&self, cx: &AppContext) -> String;
+    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
     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>>;
@@ -358,8 +358,8 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         Box::new(self.read(cx).item_handle(cx))
     }
 
-    fn title(&self, cx: &AppContext) -> String {
-        self.read(cx).title(cx)
+    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
+        self.read(cx).tab_content(style, cx)
     }
 
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {

crates/zed/assets/icons/warning.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.1329 8.29967L6.76186 0.840494C6.59383 0.553602 6.29874 0.410156 6.00365 0.410156C5.70856 0.410156 5.41347 0.553602 5.22699 0.840494L0.858047 8.29967C0.540622 8.87141 0.959504 9.59068 1.63347 9.59068H10.3755C11.0468 9.59068 11.4669 8.87346 11.1329 8.29967ZM1.83512 8.60706L5.98521 1.49215L10.1718 8.60706H1.83512ZM6.00365 6.66234C5.64791 6.66234 5.35937 6.95087 5.35937 7.30662C5.35937 7.66236 5.64852 7.95089 6.00447 7.95089C6.36042 7.95089 6.64793 7.66236 6.64793 7.30662C6.64711 6.95128 6.36022 6.66234 6.00365 6.66234ZM5.51184 3.52498V5.49223C5.51184 5.76478 5.73315 5.98405 6.00365 5.98405C6.27415 5.98405 6.49546 5.76376 6.49546 5.49223V3.52498C6.49546 3.25448 6.2762 3.03316 6.00365 3.03316C5.7311 3.03316 5.51184 3.25448 5.51184 3.52498Z" fill="white"/>
+<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0.577381 9.14286C0.414683 9.14286 0.277778 9.0873 0.166667 8.97619C0.0555556 8.86508 0 8.72817 0 8.56548C0 8.50992 0.00793651 8.45635 0.0238095 8.40476C0.0396825 8.35317 0.0595238 8.30556 0.0833333 8.2619L4.44643 0.369048C4.58135 0.123016 4.76587 0 5 0C5.23413 0 5.41865 0.123016 5.55357 0.369048L9.91667 8.2619C9.94048 8.30556 9.96032 8.35317 9.97619 8.40476C9.99206 8.45635 10 8.50992 10 8.56548C10 8.72817 9.94444 8.86508 9.83333 8.97619C9.72222 9.0873 9.58532 9.14286 9.42262 9.14286H0.577381ZM5.9881 2.40476H4.01786V3.77976L4.31548 5.61905H5.69048L5.9881 3.77976V2.40476ZM6 7.375C6 7.09722 5.90079 6.8631 5.70238 6.67262C5.50794 6.47817 5.27381 6.38095 5 6.38095C4.72619 6.38095 4.49206 6.47817 4.29762 6.67262C4.10714 6.8631 4.0119 7.09722 4.0119 7.375C4.0119 7.64881 4.10714 7.88095 4.29762 8.07143C4.49206 8.2619 4.72619 8.35714 5 8.35714C5.27381 8.35714 5.50794 8.2619 5.70238 8.07143C5.90079 7.88095 6 7.64881 6 7.375Z" fill="white"/>
 </svg>

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

@@ -316,3 +316,6 @@ message.highlight_text.color = "$text.3.color"
 background = "$surface.1"
 empty_message = "$text.0"
 status_bar_item = { extends = "$text.2", margin.right = 10 }
+tab_icon_width = 9
+tab_icon_spacing = 3
+tab_summary_spacing = 10

crates/zed/src/zed.rs 🔗

@@ -378,6 +378,10 @@ mod tests {
                     .read(cx)
                     .active_item()
                     .unwrap()
+                    .to_any()
+                    .downcast::<Editor>()
+                    .unwrap()
+                    .read(cx)
                     .title(cx),
                 "a.txt"
             );
@@ -408,6 +412,10 @@ mod tests {
                     .read(cx)
                     .active_item()
                     .unwrap()
+                    .to_any()
+                    .downcast::<Editor>()
+                    .unwrap()
+                    .read(cx)
                     .title(cx),
                 "b.txt"
             );
@@ -491,14 +499,14 @@ mod tests {
         });
 
         editor.update(&mut cx, |editor, cx| {
-            assert!(!editor.is_dirty(cx.as_ref()));
-            assert_eq!(editor.title(cx.as_ref()), "untitled");
+            assert!(!editor.is_dirty(cx));
+            assert_eq!(editor.title(cx), "untitled");
             assert!(Arc::ptr_eq(
                 editor.language(cx).unwrap(),
                 &language::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
-            assert!(editor.is_dirty(cx.as_ref()));
+            assert!(editor.is_dirty(cx));
         });
 
         // Save the buffer. This prompts for a filename.
@@ -509,7 +517,7 @@ mod tests {
         });
         cx.read(|cx| {
             assert!(editor.is_dirty(cx));
-            assert_eq!(editor.title(cx), "untitled");
+            assert_eq!(editor.read(cx).title(cx), "untitled");
         });
 
         // When the save completes, the buffer's title is updated and the language is assigned based