Remove 2 suffix for vim, diagnostics, go_to_line, theme_selector, command_palette, file_finder

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-03 10:42:49 -08:00
parent 37e6533b28
commit 252694390a
185 changed files with 1933 additions and 29192 deletions

View file

@ -10,15 +10,16 @@ doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" }
language = { package = "language2", path = "../language2" }
lsp = { package = "lsp2", path = "../lsp2" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
workspace = { path = "../workspace" }
workspace = { package = "workspace2", path = "../workspace2" }
log.workspace = true
anyhow.workspace = true
@ -30,13 +31,13 @@ smallvec.workspace = true
postage.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] }
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
serde_json.workspace = true
unindent.workspace = true

View file

@ -2,19 +2,21 @@ pub mod items;
mod project_diagnostics_settings;
mod toolbar_controls;
use anyhow::{Context, Result};
use anyhow::{Context as _, Result};
use collections::{HashMap, HashSet};
use editor::{
diagnostic_block_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
highlight_diagnostic_message,
scroll::autoscroll::Autoscroll,
Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use futures::future::try_join_all;
use gpui::{
actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
WeakView, WindowContext,
};
use language::{
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
@ -23,23 +25,22 @@ use language::{
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath};
use project_diagnostics_settings::ProjectDiagnosticsSettings;
use serde_json::json;
use smallvec::SmallVec;
use settings::Settings;
use std::{
any::{Any, TypeId},
borrow::Cow,
cmp::Ordering,
mem,
ops::Range,
path::PathBuf,
sync::Arc,
};
use theme::ThemeSettings;
use theme::ActiveTheme;
pub use toolbar_controls::ToolbarControls;
use ui::{h_stack, prelude::*, Icon, IconElement, Label};
use util::TryFutureExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
};
actions!(diagnostics, [Deploy, ToggleWarnings]);
@ -47,20 +48,18 @@ actions!(diagnostics, [Deploy, ToggleWarnings]);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut AppContext) {
settings::register::<ProjectDiagnosticsSettings>(cx);
cx.add_action(ProjectDiagnosticsEditor::deploy);
cx.add_action(ProjectDiagnosticsEditor::toggle_warnings);
items::init(cx);
ProjectDiagnosticsSettings::register(cx);
cx.observe_new_views(ProjectDiagnosticsEditor::register)
.detach();
}
type Event = editor::Event;
struct ProjectDiagnosticsEditor {
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
editor: ViewHandle<Editor>,
project: Model<Project>,
workspace: WeakView<Workspace>,
focus_handle: FocusHandle,
editor: View<Editor>,
summary: DiagnosticSummary,
excerpts: ModelHandle<MultiBuffer>,
excerpts: Model<MultiBuffer>,
path_states: Vec<PathState>,
paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
@ -89,71 +88,38 @@ struct DiagnosticGroupState {
block_count: usize,
}
impl Entity for ProjectDiagnosticsEditor {
type Event = Event;
}
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
impl View for ProjectDiagnosticsEditor {
fn ui_name() -> &'static str {
"ProjectDiagnosticsEditor"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if self.path_states.is_empty() {
let theme = &theme::current(cx).project_diagnostics;
PaneBackdrop::new(
cx.view_id(),
Label::new("No problems in workspace", theme.empty_message.clone())
.aligned()
.contained()
.with_style(theme.container)
.into_any(),
)
.into_any()
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
let child = if self.path_states.is_empty() {
div()
.bg(cx.theme().colors().editor_background)
.flex()
.items_center()
.justify_center()
.size_full()
.child(Label::new("No problems in workspace"))
} else {
ChildView::new(&self.editor, cx).into_any()
}
}
div().size_full().child(self.editor.clone())
};
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() && !self.path_states.is_empty() {
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(false, cx),
}),
"summary": self.summary,
"paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)|
(server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::<Vec<_>>())
).collect::<HashMap<_, _>>(),
"current_diagnostics": self.current_diagnostics.iter().map(|(server_id, paths)|
(server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::<Vec<_>>())
).collect::<HashMap<_, _>>(),
"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<_>>(),
})
div()
.track_focus(&self.focus_handle)
.size_full()
.on_action(cx.listener(Self::toggle_warnings))
.child(child)
}
}
impl ProjectDiagnosticsEditor {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(Self::deploy);
}
fn new(
project_handle: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
project_handle: Model<Project>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let project_event_subscription =
@ -180,19 +146,25 @@ impl ProjectDiagnosticsEditor {
_ => {}
});
let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
let editor = cx.add_view(|cx| {
let focus_handle = cx.focus_handle();
let focus_in_subscription =
cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx));
let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
let editor = cx.new_view(|cx| {
let mut editor =
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});
let editor_event_subscription = cx.subscribe(&editor, |this, _, event, cx| {
cx.emit(event.clone());
if event == &editor::Event::Focused && this.path_states.is_empty() {
cx.focus_self()
}
});
let editor_event_subscription =
cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
cx.emit(event.clone());
if event == &EditorEvent::Focused && this.path_states.is_empty() {
cx.focus(&this.focus_handle);
}
});
let project = project_handle.read(cx);
let summary = project.diagnostic_summary(false, cx);
@ -201,12 +173,17 @@ impl ProjectDiagnosticsEditor {
summary,
workspace,
excerpts,
focus_handle,
editor,
path_states: Default::default(),
paths_to_update: HashMap::default(),
include_warnings: settings::get::<ProjectDiagnosticsSettings>(cx).include_warnings,
include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
current_diagnostics: HashMap::default(),
_subscriptions: vec![project_event_subscription, editor_event_subscription],
_subscriptions: vec![
project_event_subscription,
editor_event_subscription,
focus_in_subscription,
],
};
this.update_excerpts(None, cx);
this
@ -216,8 +193,8 @@ impl ProjectDiagnosticsEditor {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
workspace.activate_item(&existing, cx);
} else {
let workspace_handle = cx.weak_handle();
let diagnostics = cx.add_view(|cx| {
let workspace_handle = cx.view().downgrade();
let diagnostics = cx.new_view(|cx| {
ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
});
workspace.add_item(Box::new(diagnostics), cx);
@ -231,6 +208,12 @@ impl ProjectDiagnosticsEditor {
cx.notify();
}
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
self.editor.focus_handle(cx).focus(cx)
}
}
fn update_excerpts(
&mut self,
language_server_id: Option<LanguageServerId>,
@ -304,9 +287,10 @@ impl ProjectDiagnosticsEditor {
let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| {
let mut cx = cx.clone();
let project = project.clone();
let this = this.clone();
async move {
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
.await
.with_context(|| format!("opening buffer for path {path:?}"))?;
this.update(&mut cx, |this, cx| {
@ -321,7 +305,7 @@ impl ProjectDiagnosticsEditor {
this.update(&mut cx, |this, cx| {
this.summary = this.project.read(cx).diagnostic_summary(false, cx);
cx.emit(Event::TitleChanged);
cx.emit(EditorEvent::TitleChanged);
})?;
anyhow::Ok(())
}
@ -334,7 +318,7 @@ impl ProjectDiagnosticsEditor {
&mut self,
path: ProjectPath,
language_server_id: Option<LanguageServerId>,
buffer: ModelHandle<Buffer>,
buffer: Model<Buffer>,
cx: &mut ViewContext<Self>,
) {
let was_empty = self.path_states.is_empty();
@ -618,41 +602,32 @@ impl ProjectDiagnosticsEditor {
});
if self.path_states.is_empty() {
if self.editor.is_focused(cx) {
cx.focus_self();
if self.editor.focus_handle(cx).is_focused(cx) {
cx.focus(&self.focus_handle);
}
} else if cx.handle().is_focused(cx) {
cx.focus(&self.editor);
} else if self.focus_handle.is_focused(cx) {
let focus_handle = self.editor.focus_handle(cx);
cx.focus(&focus_handle);
}
cx.notify();
}
}
impl FocusableView for ProjectDiagnosticsEditor {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ProjectDiagnosticsEditor {
fn tab_content<T: 'static>(
&self,
_detail: Option<usize>,
style: &theme::Tab,
cx: &AppContext,
) -> AnyElement<T> {
render_summary(
&self.summary,
&style.label.text,
&theme::current(cx).project_diagnostics,
)
type Event = EditorEvent;
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &AppContext) -> bool {
false
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
@ -660,10 +635,82 @@ impl Item for ProjectDiagnosticsEditor {
.update(cx, |editor, cx| editor.navigate(data, cx))
}
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
Some("Project Diagnostics".into())
}
fn tab_content(&self, _detail: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
let label = Label::new("No problems");
label.into_any_element()
} else {
h_stack()
.gap_1()
.when(self.summary.error_count > 0, |then| {
then.child(
h_stack()
.gap_1()
.child(IconElement::new(Icon::XCircle).color(Color::Error))
.child(Label::new(self.summary.error_count.to_string()).color(
if selected {
Color::Default
} else {
Color::Muted
},
)),
)
})
.when(self.summary.warning_count > 0, |then| {
then.child(
h_stack()
.gap_1()
.child(
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
)
.child(Label::new(self.summary.warning_count.to_string()).color(
if selected {
Color::Default
} else {
Color::Muted
},
)),
)
})
.into_any_element()
}
}
fn for_each_project_item(
&self,
cx: &AppContext,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
) {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &AppContext) -> bool {
false
}
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where
Self: Sized,
{
Some(cx.new_view(|cx| {
ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
}))
}
fn is_dirty(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).is_dirty(cx)
}
@ -676,209 +723,133 @@ impl Item for ProjectDiagnosticsEditor {
true
}
fn save(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.editor.save(project, cx)
}
fn reload(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.editor.reload(project, cx)
}
fn save_as(
&mut self,
_: ModelHandle<Project>,
_: Model<Project>,
_: PathBuf,
_: &mut ViewContext<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
Editor::to_item_events(event)
}
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{
Some(ProjectDiagnosticsEditor::new(
self.project.clone(),
self.workspace.clone(),
cx,
))
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.editor.reload(project, cx)
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a ViewHandle<Self>,
self_handle: &'a View<Self>,
_: &'a AppContext,
) -> Option<&AnyViewHandle> {
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle)
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(&self.editor)
Some(self.editor.to_any())
} else {
None
}
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
}
fn serialized_item_kind() -> Option<&'static str> {
Some("diagnostics")
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft { flex: None }
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
}
fn serialized_item_kind() -> Option<&'static str> {
Some("diagnostics")
}
fn deserialize(
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
project: Model<Project>,
workspace: WeakView<Workspace>,
_workspace_id: workspace::WorkspaceId,
_item_id: workspace::ItemId,
cx: &mut ViewContext<Pane>,
) -> Task<Result<ViewHandle<Self>>> {
Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
) -> Task<Result<View<Self>>> {
Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
}
}
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic);
let message: SharedString = message.into();
Arc::new(move |cx| {
let settings = settings::get::<ThemeSettings>(cx);
let theme = &settings.theme.editor;
let style = theme.diagnostic_header.clone();
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
let icon_width = cx.em_width * style.icon_width_factor;
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
Svg::new("icons/error.svg").with_color(theme.error_diagnostic.message.text.color)
} else {
Svg::new("icons/warning.svg").with_color(theme.warning_diagnostic.message.text.color)
};
Flex::row()
.with_child(
icon.constrained()
.with_width(icon_width)
.aligned()
.contained()
.with_margin_right(cx.gutter_padding),
let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
h_stack()
.id("diagnostic header")
.py_2()
.pl_10()
.pr_5()
.w_full()
.justify_between()
.gap_2()
.child(
h_stack()
.gap_3()
.map(|stack| {
stack.child(
svg()
.size(cx.text_style().font_size)
.flex_none()
.map(|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR {
icon.path(Icon::XCircle.path())
.text_color(Color::Error.color(cx))
} else {
icon.path(Icon::ExclamationTriangle.path())
.text_color(Color::Warning.color(cx))
}
}),
)
})
.child(
h_stack()
.gap_1()
.child(
StyledText::new(message.clone()).with_highlights(
&cx.text_style(),
code_ranges
.iter()
.map(|range| (range.clone(), highlight_style)),
),
)
.when_some(diagnostic.code.as_ref(), |stack, code| {
stack.child(
div()
.child(SharedString::from(format!("({code})")))
.text_color(cx.theme().colors().text_muted),
)
}),
),
)
.with_children(diagnostic.source.as_ref().map(|source| {
Label::new(
format!("{source}: "),
style.source.label.clone().with_font_size(font_size),
)
.contained()
.with_style(style.message.container)
.aligned()
}))
.with_child(
Label::new(
message.clone(),
style.message.label.clone().with_font_size(font_size),
)
.with_highlights(highlights.clone())
.contained()
.with_style(style.message.container)
.aligned(),
.child(
h_stack()
.gap_1()
.when_some(diagnostic.source.as_ref(), |stack, source| {
stack.child(
div()
.child(SharedString::from(source.clone()))
.text_color(cx.theme().colors().text_muted),
)
}),
)
.with_children(diagnostic.code.clone().map(|code| {
Label::new(code, style.code.text.clone().with_font_size(font_size))
.contained()
.with_style(style.code.container)
.aligned()
}))
.contained()
.with_style(style.container)
.with_padding_left(cx.gutter_padding)
.with_padding_right(cx.gutter_padding)
.expanded()
.into_any_named("diagnostic header")
.into_any_element()
})
}
pub(crate) fn render_summary<T: 'static>(
summary: &DiagnosticSummary,
text_style: &TextStyle,
theme: &theme::ProjectDiagnostics,
) -> AnyElement<T> {
if summary.error_count == 0 && summary.warning_count == 0 {
Label::new("No problems", text_style.clone()).into_any()
} else {
let icon_width = theme.tab_icon_width;
let icon_spacing = theme.tab_icon_spacing;
let summary_spacing = theme.tab_summary_spacing;
Flex::row()
.with_child(
Svg::new("icons/error.svg")
.with_color(text_style.color)
.constrained()
.with_width(icon_width)
.aligned()
.contained()
.with_margin_right(icon_spacing),
)
.with_child(
Label::new(
summary.error_count.to_string(),
LabelStyle {
text: text_style.clone(),
highlight_text: None,
},
)
.aligned(),
)
.with_child(
Svg::new("icons/warning.svg")
.with_color(text_style.color)
.constrained()
.with_width(icon_width)
.aligned()
.contained()
.with_margin_left(summary_spacing)
.with_margin_right(icon_spacing),
)
.with_child(
Label::new(
summary.warning_count.to_string(),
LabelStyle {
text: text_style.clone(),
highlight_text: None,
},
)
.aligned(),
)
.into_any()
}
}
fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
lhs: &DiagnosticEntry<L>,
rhs: &DiagnosticEntry<R>,
@ -904,7 +875,7 @@ mod tests {
display_map::{BlockContext, TransformBlock},
DisplayPoint,
};
use gpui::{TestAppContext, WindowContext};
use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
use project::FakeFs;
use serde_json::json;
@ -915,7 +886,7 @@ mod tests {
async fn test_diagnostics(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/test",
json!({
@ -945,7 +916,8 @@ mod tests {
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
// Create some diagnostics
project.update(cx, |project, cx| {
@ -1032,7 +1004,7 @@ mod tests {
});
// Open the project diagnostics view while there are already diagnostics.
let view = window.add_view(cx, |cx| {
let view = window.build_view(cx, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
@ -1320,7 +1292,7 @@ mod tests {
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/test",
json!({
@ -1339,9 +1311,10 @@ mod tests {
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let view = window.add_view(cx, |cx| {
let view = window.build_view(cx, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
@ -1376,7 +1349,7 @@ mod tests {
});
// Only the first language server's diagnostics are shown.
cx.foreground().run_until_parked();
cx.executor().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
@ -1424,7 +1397,7 @@ mod tests {
});
// Both language server's diagnostics are shown.
cx.foreground().run_until_parked();
cx.executor().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
@ -1492,7 +1465,7 @@ mod tests {
});
// Only the first language server's diagnostics are updated.
cx.foreground().run_until_parked();
cx.executor().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
@ -1550,7 +1523,7 @@ mod tests {
});
// Both language servers' diagnostics are updated.
cx.foreground().run_until_parked();
cx.executor().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
@ -1586,8 +1559,9 @@ mod tests {
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
let settings = SettingsStore::test(cx);
cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
client::init_settings(cx);
workspace::init_settings(cx);
@ -1596,7 +1570,7 @@ mod tests {
});
}
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
snapshot
@ -1607,23 +1581,25 @@ mod tests {
TransformBlock::Custom(block) => block
.render(&mut BlockContext {
view_context: cx,
anchor_x: 0.,
scroll_x: 0.,
gutter_padding: 0.,
gutter_width: 0.,
line_height: 0.,
em_width: 0.,
anchor_x: px(0.),
gutter_padding: px(0.),
gutter_width: px(0.),
line_height: px(0.),
em_width: px(0.),
block_id: ix,
editor_style: &editor::EditorStyle::default(),
})
.name()?
.to_string(),
.inner_id()?
.try_into()
.ok()?,
TransformBlock::ExcerptHeader {
starts_new_buffer, ..
} => {
if *starts_new_buffer {
"path header block".to_string()
"path header block".into()
} else {
"collapsed context".to_string()
"collapsed context".into()
}
}
};

View file

@ -1,27 +1,105 @@
use collections::HashSet;
use editor::{Editor, GoToDiagnostic};
use editor::Editor;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
serde_json, AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
ViewContext, WeakView,
};
use language::Diagnostic;
use lsp::LanguageServerId;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
use crate::ProjectDiagnosticsEditor;
use crate::{Deploy, ProjectDiagnosticsEditor};
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
active_editor: Option<WeakViewHandle<Editor>>,
workspace: WeakViewHandle<Workspace>,
active_editor: Option<WeakView<Editor>>,
workspace: WeakView<Workspace>,
current_diagnostic: Option<Diagnostic>,
in_progress_checks: HashSet<LanguageServerId>,
_observe_active_editor: Option<Subscription>,
}
pub fn init(cx: &mut AppContext) {
cx.add_action(DiagnosticIndicator::go_to_next_diagnostic);
impl Render for DiagnosticIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_stack().child(
IconElement::new(Icon::Check)
.size(IconSize::Small)
.color(Color::Success),
),
(0, warning_count) => h_stack()
.gap_1()
.child(
IconElement::new(Icon::ExclamationTriangle)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
(error_count, 0) => h_stack()
.gap_1()
.child(
IconElement::new(Icon::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.child(Label::new(error_count.to_string()).size(LabelSize::Small)),
(error_count, warning_count) => h_stack()
.gap_1()
.child(
IconElement::new(Icon::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
.child(
IconElement::new(Icon::ExclamationTriangle)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
};
let status = if !self.in_progress_checks.is_empty() {
Some(
Label::new("Checking…")
.size(LabelSize::Small)
.into_any_element(),
)
} else if let Some(diagnostic) = &self.current_diagnostic {
let message = diagnostic.message.split('\n').next().unwrap().to_string();
Some(
Button::new("diagnostic_message", message)
.label_size(LabelSize::Small)
.tooltip(|cx| {
Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx)
})
.on_click(cx.listener(|this, _, cx| {
this.go_to_next_diagnostic(cx);
}))
.into_any_element(),
)
} else {
None
};
h_stack()
.h(rems(1.375))
.gap_2()
.child(
ButtonLike::new("diagnostic-indicator")
.child(diagnostic_indicator)
.tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
})
}
})),
)
.children(status)
}
}
impl DiagnosticIndicator {
@ -32,19 +110,23 @@ impl DiagnosticIndicator {
this.in_progress_checks.insert(*language_server_id);
cx.notify();
}
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
| project::Event::LanguageServerRemoved(language_server_id) => {
this.summary = project.read(cx).diagnostic_summary(false, cx);
this.in_progress_checks.remove(language_server_id);
cx.notify();
}
project::Event::DiagnosticsUpdated { .. } => {
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.notify();
}
_ => {}
})
.detach();
Self {
summary: project.read(cx).diagnostic_summary(false, cx),
in_progress_checks: project
@ -58,15 +140,15 @@ impl DiagnosticIndicator {
}
}
fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade(cx)) {
fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
editor.update(cx, |editor, cx| {
editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
})
}
}
fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx);
let cursor_position = editor.selections.newest::<usize>(cx).head();
@ -83,146 +165,7 @@ impl DiagnosticIndicator {
}
}
impl Entity for DiagnosticIndicator {
type Event = ();
}
impl View for DiagnosticIndicator {
fn ui_name() -> &'static str {
"DiagnosticIndicator"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Summary {}
enum Message {}
let tooltip_style = theme::current(cx).tooltip.clone();
let in_progress = !self.in_progress_checks.is_empty();
let mut element = Flex::row().with_child(
MouseEventHandler::new::<Summary, _>(0, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
.workspace
.status_bar
.diagnostic_summary
.style_for(state);
let mut summary_row = Flex::row();
if self.summary.error_count > 0 {
summary_row.add_child(
Svg::new("icons/error.svg")
.with_color(style.icon_color_error)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_margin_right(style.icon_spacing),
);
summary_row.add_child(
Label::new(self.summary.error_count.to_string(), style.text.clone())
.aligned(),
);
}
if self.summary.warning_count > 0 {
summary_row.add_child(
Svg::new("icons/warning.svg")
.with_color(style.icon_color_warning)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_margin_right(style.icon_spacing)
.with_margin_left(if self.summary.error_count > 0 {
style.summary_spacing
} else {
0.
}),
);
summary_row.add_child(
Label::new(self.summary.warning_count.to_string(), style.text.clone())
.aligned(),
);
}
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
summary_row.add_child(
Svg::new("icons/check_circle.svg")
.with_color(style.icon_color_ok)
.constrained()
.with_width(style.icon_width)
.aligned()
.into_any_named("ok-icon"),
);
}
summary_row
.constrained()
.with_height(style.height)
.contained()
.with_style(if self.summary.error_count > 0 {
style.container_error
} else if self.summary.warning_count > 0 {
style.container_warning
} else {
style.container_ok
})
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
})
}
})
.with_tooltip::<Summary>(
0,
"Project Diagnostics",
Some(Box::new(crate::Deploy)),
tooltip_style,
cx,
)
.aligned()
.into_any(),
);
let style = &theme::current(cx).workspace.status_bar;
let item_spacing = style.item_spacing;
if in_progress {
element.add_child(
Label::new("Checking…", style.diagnostic_message.default.text.clone())
.aligned()
.contained()
.with_margin_left(item_spacing),
);
} else if let Some(diagnostic) = &self.current_diagnostic {
let message_style = style.diagnostic_message.clone();
element.add_child(
MouseEventHandler::new::<Message, _>(1, cx, |state, _| {
Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(),
message_style.style_for(state).text.clone(),
)
.aligned()
.contained()
.with_margin_left(item_spacing)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.go_to_next_diagnostic(&Default::default(), cx)
}),
);
}
element.into_any_named("diagnostic indicator")
}
fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
serde_json::json!({ "summary": self.summary })
}
}
impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
impl StatusItemView for DiagnosticIndicator {
fn set_active_pane_item(

View file

@ -11,14 +11,14 @@ pub struct ProjectDiagnosticsSettingsContent {
include_warnings: Option<bool>,
}
impl settings::Setting for ProjectDiagnosticsSettings {
impl settings::Settings for ProjectDiagnosticsSettings {
const KEY: Option<&'static str> = Some("diagnostics");
type FileContent = ProjectDiagnosticsSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_cx: &gpui::AppContext,
_cx: &mut gpui::AppContext,
) -> anyhow::Result<Self>
where
Self: Sized,

View file

@ -1,55 +1,44 @@
use crate::{ProjectDiagnosticsEditor, ToggleWarnings};
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
Action, Entity, EventContext, View, ViewContext, WeakViewHandle,
};
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::ProjectDiagnosticsEditor;
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
use ui::prelude::*;
use ui::{Icon, IconButton, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub struct ToolbarControls {
editor: Option<WeakViewHandle<ProjectDiagnosticsEditor>>,
editor: Option<WeakView<ProjectDiagnosticsEditor>>,
}
impl Entity for ToolbarControls {
type Event = ();
}
impl View for ToolbarControls {
fn ui_name() -> &'static str {
"ToolbarControls"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
impl Render for ToolbarControls {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let include_warnings = self
.editor
.as_ref()
.and_then(|editor| editor.upgrade(cx))
.and_then(|editor| editor.upgrade())
.map(|editor| editor.read(cx).include_warnings)
.unwrap_or(false);
let tooltip = if include_warnings {
"Exclude Warnings".into()
"Exclude Warnings"
} else {
"Include Warnings".into()
"Include Warnings"
};
Flex::row()
.with_child(render_toggle_button(
0,
"icons/warning.svg",
include_warnings,
(tooltip, Some(Box::new(ToggleWarnings))),
cx,
move |this, cx| {
if let Some(editor) = this.editor.and_then(|editor| editor.upgrade(cx)) {
div().child(
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx)
editor.toggle_warnings(&Default::default(), cx);
});
}
},
))
.into_any()
})),
)
}
}
impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
impl ToolbarItemView for ToolbarControls {
fn set_active_pane_item(
&mut self,
@ -59,7 +48,7 @@ impl ToolbarItemView for ToolbarControls {
if let Some(pane_item) = active_pane_item.as_ref() {
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
self.editor = Some(editor.downgrade());
ToolbarItemLocation::PrimaryRight { flex: None }
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
@ -74,42 +63,3 @@ impl ToolbarControls {
ToolbarControls { editor: None }
}
}
fn render_toggle_button<
F: 'static + Fn(&mut ToolbarControls, &mut EventContext<ToolbarControls>),
>(
index: usize,
icon: &'static str,
toggled: bool,
tooltip: (String, Option<Box<dyn Action>>),
cx: &mut ViewContext<ToolbarControls>,
on_click: F,
) -> AnyElement<ToolbarControls> {
enum Button {}
let theme = theme::current(cx);
let (tooltip_text, action) = tooltip;
MouseEventHandler::new::<Button, _>(index, cx, |mouse_state, _| {
let style = theme
.workspace
.toolbar
.toggleable_tool
.in_state(toggled)
.style_for(mouse_state);
Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| on_click(view, cx))
.with_tooltip::<Button>(index, tooltip_text, action, theme.tooltip.clone(), cx)
.into_any_named("quick action bar button")
}