From 6865a42df9718498f136c03e512c9710e62ce494 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 11 Jan 2022 17:23:11 -0800 Subject: [PATCH] Show error+warning counts in project diagnostics tab Allow workspace items' tab contents to be arbitrary elements Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/diagnostics/Cargo.toml | 1 + crates/diagnostics/src/diagnostics.rs | 52 ++++++++++++++++++++------- crates/editor/src/editor.rs | 19 ++++++++-- crates/editor/src/items.rs | 19 +++------- crates/file_finder/src/file_finder.rs | 10 +++++- crates/theme/src/theme.rs | 3 ++ crates/workspace/src/pane.rs | 48 ++++++++----------------- crates/workspace/src/workspace.rs | 14 +++++--- crates/zed/assets/icons/no.svg | 4 +++ crates/zed/assets/icons/warning.svg | 4 +-- crates/zed/assets/themes/_base.toml | 3 ++ crates/zed/src/zed.rs | 20 +++++++---- 13 files changed, 121 insertions(+), 77 deletions(-) create mode 100644 crates/zed/assets/icons/no.svg diff --git a/Cargo.lock b/Cargo.lock index 8c3174d68d..24fe3a6d9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1412,6 +1412,7 @@ dependencies = [ "postage", "project", "serde_json", + "theme", "unindent", "util", "workspace", diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 5da4c9c8fa..df3022ef43 100644 --- a/crates/diagnostics/Cargo.toml +++ b/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"] } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 7fc94286ff..d5ebd1cfdb 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -14,7 +14,7 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal}; use postage::watch; -use project::{Project, ProjectPath, WorktreeId}; +use project::{DiagnosticSummary, Project, ProjectPath, WorktreeId}; use std::{cmp::Ordering, mem, ops::Range, sync::Arc}; use util::TryFutureExt; use workspace::Workspace; @@ -47,6 +47,7 @@ struct ProjectDiagnosticsEditor { model: ModelHandle, workspace: WeakViewHandle, editor: ViewHandle, + summary: DiagnosticSummary, excerpts: ModelHandle, path_states: Vec, paths_to_update: HashMap>, @@ -120,9 +121,11 @@ impl ProjectDiagnosticsEditor { let project = model.read(cx).project.clone(); cx.subscribe(&project, |this, _, event, cx| match event { project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => { + this.summary = this.model.read(cx).project.read(cx).diagnostic_summary(cx); if let Some(paths) = this.paths_to_update.remove(&worktree_id) { this.update_excerpts(paths, cx); } + cx.emit(Event::TitleChanged) } project::Event::DiagnosticsUpdated(path) => { this.paths_to_update @@ -141,13 +144,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, @@ -544,8 +545,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 { @@ -586,10 +617,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) } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index eb412f3dcb..90c0b00b71 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -535,6 +535,19 @@ impl Editor { &self.buffer } + pub fn title(&self, cx: &AppContext) -> String { + let filename = self + .buffer() + .read(cx) + .file(cx) + .and_then(|file| file.file_name()); + 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, @@ -3619,8 +3632,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), _ => {} } @@ -3727,7 +3740,7 @@ pub enum Event { Blurred, Dirtied, Saved, - FileHandleChanged, + TitleChanged, Closed, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8abcc76ef9..74641a7288 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -102,17 +102,9 @@ impl ItemView for Editor { BufferItemHandle(self.buffer.read(cx).as_singleton().unwrap()) } - fn title(&self, cx: &AppContext) -> String { - let filename = self - .buffer() - .read(cx) - .file(cx) - .and_then(|file| file.file_name()); - if let Some(name) = filename { - name.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 { @@ -218,10 +210,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) } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 707a3bfb20..3f3a57e3cc 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -491,7 +491,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::() + .unwrap() + .read(cx) + .title(cx), + "bandana" + ); }); } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3bd78f2634..acefeaad0d 100644 --- a/crates/theme/src/theme.rs +++ b/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)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 731db29d63..39ab5848cf 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -55,8 +55,6 @@ pub enum Event { Split(SplitDirection), } -const MAX_TAB_TITLE_LEN: usize = 24; - #[derive(Debug, Eq, PartialEq)] pub struct State { pub tabs: Vec, @@ -213,15 +211,12 @@ impl Pane { let is_active = ix == self.active_item; 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() @@ -270,29 +265,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( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index eec0a1d446..9a69959491 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -142,7 +142,7 @@ pub trait ItemView: View { type ItemHandle: ItemHandle; 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; fn clone_on_split(&self, _: &mut ViewContext) -> Option where @@ -197,7 +197,7 @@ pub trait WeakItemHandle { pub trait ItemViewHandle { fn item_handle(&self, cx: &AppContext) -> Box; - fn title(&self, cx: &AppContext) -> String; + fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; @@ -308,8 +308,8 @@ impl ItemViewHandle for ViewHandle { 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 { @@ -980,7 +980,11 @@ impl Workspace { } pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { - let ix = self.panes.iter().position(|pane| pane == &self.active_pane).unwrap(); + let ix = self + .panes + .iter() + .position(|pane| pane == &self.active_pane) + .unwrap(); let next_ix = (ix + 1) % self.panes.len(); self.activate_pane(self.panes[next_ix].clone(), cx); } diff --git a/crates/zed/assets/icons/no.svg b/crates/zed/assets/icons/no.svg new file mode 100644 index 0000000000..799a6dcc0f --- /dev/null +++ b/crates/zed/assets/icons/no.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/zed/assets/icons/warning.svg b/crates/zed/assets/icons/warning.svg index 09ebc28669..845d07a15a 100644 --- a/crates/zed/assets/icons/warning.svg +++ b/crates/zed/assets/icons/warning.svg @@ -1,3 +1,3 @@ - - + + diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 95dc7eee60..f3e65cdf41 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -281,3 +281,6 @@ border = { width = 1, top = true, color = "$border.0" } 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 diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 61300d1f56..9c7c4410fd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -382,6 +382,10 @@ mod tests { .read(cx) .active_item() .unwrap() + .to_any() + .downcast::() + .unwrap() + .read(cx) .title(cx), "a.txt" ); @@ -413,6 +417,10 @@ mod tests { .read(cx) .active_item() .unwrap() + .to_any() + .downcast::() + .unwrap() + .read(cx) .title(cx), "b.txt" ); @@ -502,14 +510,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. @@ -522,7 +530,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. @@ -531,7 +539,7 @@ mod tests { .await; cx.read(|cx| { assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "the-new-name.rs"); + assert_eq!(editor.read(cx).title(cx), "the-new-name.rs"); }); // The language is assigned based on the path editor.read_with(&cx, |editor, cx| { @@ -550,7 +558,7 @@ mod tests { editor .condition(&cx, |editor, cx| !editor.is_dirty(cx)) .await; - cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs")); + cx.read(|cx| assert_eq!(editor.read(cx).title(cx), "the-new-name.rs")); // Open the same newly-created file in another pane item. The new editor should reuse // the same buffer.