diff --git a/Cargo.lock b/Cargo.lock index 9fcf1c4d63..9417057bba 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 6b168ea170..a6c351002d 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/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, workspace: WeakViewHandle, editor: ViewHandle, + summary: DiagnosticSummary, excerpts: ModelHandle, path_states: Vec, paths_to_update: BTreeSet, @@ -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 { @@ -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) -> Option diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 323f9ce3c4..3f38f82aa7 100644 --- a/crates/editor/src/editor.rs +++ b/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, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index a2413f248a..82a398f6be 100644 --- a/crates/editor/src/items.rs +++ b/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 { @@ -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) } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 588a80593e..3bf489df91 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/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::() + .unwrap() + .read(cx) + .title(cx), + "bandana" + ); }); } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5d6142b9e4..497e9f9458 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 17f370ac4f..739d07aab1 100644 --- a/crates/workspace/src/pane.rs +++ b/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)>, active_item_index: usize, @@ -79,6 +77,11 @@ pub struct Pane { nav_history: Rc>, } +// #[derive(Debug, Eq, PartialEq)] +// pub struct State { +// pub tabs: Vec, +// } + pub struct ItemNavHistory { history: Rc>, item_view: Rc, @@ -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( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 86f399a86a..48dcb4907b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -155,7 +155,7 @@ pub trait ItemView: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} 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 @@ -223,7 +223,7 @@ pub trait WeakItemHandle { pub trait ItemViewHandle: 'static { 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>; @@ -358,8 +358,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 { 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 b9858b504f..37c878969c 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/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 diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fc7ae9a3c5..d51376afb0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -378,6 +378,10 @@ mod tests { .read(cx) .active_item() .unwrap() + .to_any() + .downcast::() + .unwrap() + .read(cx) .title(cx), "a.txt" ); @@ -408,6 +412,10 @@ mod tests { .read(cx) .active_item() .unwrap() + .to_any() + .downcast::() + .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