Show error+warning counts in project diagnostics tab

Allow workspace items' tab contents to be arbitrary elements

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2022-01-11 17:23:11 -08:00
parent 6ad9ff10c1
commit 6865a42df9
13 changed files with 121 additions and 77 deletions

1
Cargo.lock generated
View file

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

View file

@ -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"] }

View file

@ -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<ProjectDiagnostics>,
workspace: WeakViewHandle<Workspace>,
editor: ViewHandle<Editor>,
summary: DiagnosticSummary,
excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<PathState>,
paths_to_update: HashMap<WorktreeId, BTreeSet<ProjectPath>>,
@ -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<project::ProjectPath> {
@ -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)
}
}

View file

@ -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,
}

View file

@ -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<ProjectPath> {
@ -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)
}
}

View file

@ -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::<Editor>()
.unwrap()
.read(cx)
.title(cx),
"bandana"
);
});
}

View file

@ -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)]

View file

@ -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<TabState>,
@ -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(

View file

@ -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<ProjectPath>;
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
where
@ -197,7 +197,7 @@ pub trait WeakItemHandle {
pub trait ItemViewHandle {
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>>;
@ -308,8 +308,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> {
@ -980,7 +980,11 @@ impl Workspace {
}
pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
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);
}

View file

@ -0,0 +1,4 @@
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 9C4.11176 9 3.72353 8.95294 3.33529 8.85882C2.94706 8.75294 2.58235 8.6 2.24118 8.4C1.9 8.2 1.58824 7.96471 1.30588 7.69412C1.03529 7.41177 0.8 7.1 0.6 6.75882C0.4 6.41765 0.247059 6.05294 0.141177 5.66471C0.0470589 5.27647 0 4.88824 0 4.5C0 4.11176 0.0470589 3.72353 0.141177 3.33529C0.247059 2.94706 0.4 2.58235 0.6 2.24118C0.8 1.9 1.03529 1.59412 1.30588 1.32353C1.58824 1.04118 1.9 0.799999 2.24118 0.599999C2.58235 0.4 2.94706 0.252941 3.33529 0.158823C3.72353 0.0529408 4.11176 0 4.5 0C4.88824 0 5.27647 0.0529408 5.66471 0.158823C6.05294 0.252941 6.41765 0.4 6.75882 0.599999C7.1 0.799999 7.40588 1.04118 7.67647 1.32353C7.95882 1.59412 8.2 1.9 8.4 2.24118C8.6 2.58235 8.74706 2.94706 8.84118 3.33529C8.94706 3.72353 9 4.11176 9 4.5C9 4.88824 8.94706 5.27647 8.84118 5.66471C8.74706 6.05294 8.6 6.41765 8.4 6.75882C8.2 7.1 7.95882 7.41177 7.67647 7.69412C7.40588 7.96471 7.1 8.2 6.75882 8.4C6.41765 8.6 6.05294 8.75294 5.66471 8.85882C5.27647 8.95294 4.88824 9 4.5 9ZM1.97647 5.92941L6.03529 1.88823C5.81176 1.72353 5.56471 1.6 5.29412 1.51765C5.03529 1.43529 4.77059 1.39412 4.5 1.39412C4.11176 1.39412 3.73529 1.48235 3.37059 1.65882C3.01765 1.82353 2.71177 2.05294 2.45294 2.34706C2.19412 2.64118 1.99412 2.97647 1.85294 3.35294C1.72353 3.72941 1.65882 4.11176 1.65882 4.5C1.65882 4.74706 1.68235 4.99412 1.72941 5.24118C1.78824 5.47647 1.87059 5.70588 1.97647 5.92941ZM4.5 7.60588C4.88824 7.60588 5.25882 7.52353 5.61176 7.35882C5.97647 7.18235 6.28824 6.94706 6.54706 6.65294C6.80588 6.35882 7 6.02353 7.12941 5.64706C7.27059 5.27059 7.34118 4.88824 7.34118 4.5C7.34118 4.25294 7.31177 4.01177 7.25294 3.77647C7.20588 3.52941 7.12941 3.29412 7.02353 3.07059L2.96471 7.11177C3.18824 7.27647 3.42941 7.4 3.68824 7.48235C3.95882 7.56471 4.22941 7.60588 4.5 7.60588Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -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>

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -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

View file

@ -382,6 +382,10 @@ mod tests {
.read(cx)
.active_item()
.unwrap()
.to_any()
.downcast::<Editor>()
.unwrap()
.read(cx)
.title(cx),
"a.txt"
);
@ -413,6 +417,10 @@ mod tests {
.read(cx)
.active_item()
.unwrap()
.to_any()
.downcast::<Editor>()
.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.