diff --git a/Cargo.lock b/Cargo.lock index 8b14cf6da3..3b070c56e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1906,27 +1906,6 @@ dependencies = [ [[package]] name = "command_palette" version = "0.1.0" -dependencies = [ - "collections", - "ctor", - "editor", - "env_logger", - "fuzzy", - "gpui", - "language", - "picker", - "project", - "serde_json", - "settings", - "theme", - "util", - "workspace", - "zed-actions", -] - -[[package]] -name = "command_palette2" -version = "0.1.0" dependencies = [ "anyhow", "collections", @@ -1934,7 +1913,7 @@ dependencies = [ "editor2", "env_logger", "fuzzy2", - "go_to_line2", + "go_to_line", "gpui2", "language2", "menu2", @@ -2623,33 +2602,6 @@ dependencies = [ [[package]] name = "diagnostics" version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "editor", - "futures 0.3.28", - "gpui", - "language", - "log", - "lsp", - "postage", - "project", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings", - "smallvec", - "theme", - "unindent", - "util", - "workspace", -] - -[[package]] -name = "diagnostics2" -version = "0.1.0" dependencies = [ "anyhow", "client2", @@ -3166,29 +3118,6 @@ dependencies = [ [[package]] name = "file_finder" version = "0.1.0" -dependencies = [ - "collections", - "ctor", - "editor", - "env_logger", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "postage", - "project", - "serde_json", - "settings", - "text", - "theme", - "util", - "workspace", -] - -[[package]] -name = "file_finder2" -version = "0.1.0" dependencies = [ "collections", "ctor", @@ -3745,21 +3674,6 @@ dependencies = [ [[package]] name = "go_to_line" version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "menu", - "postage", - "settings", - "text", - "theme", - "util", - "workspace", -] - -[[package]] -name = "go_to_line2" -version = "0.1.0" dependencies = [ "editor2", "gpui2", @@ -10540,40 +10454,6 @@ dependencies = [ "collections", "command_palette", "diagnostics", - "editor", - "futures 0.3.28", - "gpui", - "indoc", - "itertools 0.10.5", - "language", - "language_selector", - "log", - "lsp", - "nvim-rs", - "parking_lot 0.11.2", - "project", - "search", - "serde", - "serde_derive", - "serde_json", - "settings", - "theme", - "tokio", - "util", - "workspace", - "zed-actions", -] - -[[package]] -name = "vim2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compat", - "async-trait", - "collections", - "command_palette2", - "diagnostics2", "editor2", "futures 0.3.28", "gpui2", @@ -11007,7 +10887,7 @@ dependencies = [ "theme_selector", "ui2", "util", - "vim2", + "vim", "workspace2", ] @@ -11429,21 +11309,21 @@ dependencies = [ "client2", "collab_ui", "collections", - "command_palette2", + "command_palette", "copilot2", "copilot_button2", "ctor", "db2", - "diagnostics2", + "diagnostics", "editor2", "env_logger", "feature_flags2", "feedback2", - "file_finder2", + "file_finder", "fs2", "fsevent", "futures 0.3.28", - "go_to_line2", + "go_to_line", "gpui2", "ignore", "image", @@ -11530,7 +11410,7 @@ dependencies = [ "urlencoding", "util", "uuid 1.4.1", - "vim2", + "vim", "welcome", "workspace2", "zed_actions2", diff --git a/Cargo.toml b/Cargo.toml index 0c3ec653fd..b117e09998 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", - "crates/command_palette2", "crates/component_test", "crates/context_menu", "crates/copilot", @@ -35,7 +34,6 @@ members = [ "crates/refineable", "crates/refineable/derive_refineable", "crates/diagnostics", - "crates/diagnostics2", "crates/drag_and_drop", "crates/editor", "crates/feature_flags", @@ -49,7 +47,6 @@ members = [ "crates/fuzzy2", "crates/git", "crates/go_to_line", - "crates/go_to_line2", "crates/gpui", "crates/gpui_macros", "crates/gpui2", diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index b42a3b5f41..c16765f594 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -10,23 +10,28 @@ doctest = false [dependencies] collections = { path = "../collections" } -editor = { path = "../editor" } -fuzzy = { path = "../fuzzy" } -gpui = { path = "../gpui" } -picker = { path = "../picker" } -project = { path = "../project" } -settings = { path = "../settings" } +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } -theme = { path = "../theme" } -workspace = { path = "../workspace" } -zed-actions = { path = "../zed-actions" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package="workspace2", path = "../workspace2" } +zed_actions = { package = "zed_actions2", path = "../zed_actions2" } +anyhow.workspace = true +serde.workspace = true [dev-dependencies] -gpui = { path = "../gpui", features = ["test-support"] } -editor = { path = "../editor", features = ["test-support"] } -language = { path = "../language", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +language = { package="language2", path = "../language2", features = ["test-support"] } +project = { package="project2", path = "../project2", features = ["test-support"] } +menu = { package = "menu2", path = "../menu2" } +go_to_line = { path = "../go_to_line" } serde_json.workspace = true -workspace = { path = "../workspace", features = ["test-support"] } +workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 356300052e..b7a1dbfd3d 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,26 +1,92 @@ +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; + use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, anyhow::anyhow, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle, - AppContext, Element, MouseState, ViewContext, + actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, + ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, }; -use picker::{Picker, PickerDelegate, PickerEvent}; -use std::cmp::{self, Reverse}; +use picker::{Picker, PickerDelegate}; + +use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, }; -use workspace::Workspace; +use workspace::{ModalView, Workspace}; use zed_actions::OpenZedURL; -pub fn init(cx: &mut AppContext) { - cx.add_action(toggle_command_palette); - CommandPalette::init(cx); -} - actions!(command_palette, [Toggle]); -pub type CommandPalette = Picker; +pub fn init(cx: &mut AppContext) { + cx.set_global(HitCounts::default()); + cx.set_global(CommandPaletteFilter::default()); + cx.observe_new_views(CommandPalette::register).detach(); +} + +impl ModalView for CommandPalette {} + +pub struct CommandPalette { + picker: View>, +} + +impl CommandPalette { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + let Some(previous_focus_handle) = cx.focused() else { + return; + }; + workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx)); + }); + } + + fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext) -> Self { + let filter = cx.try_global::(); + + let commands = cx + .available_actions() + .into_iter() + .filter_map(|action| { + let name = action.name(); + let namespace = name.split("::").next().unwrap_or("malformed action name"); + if filter.is_some_and(|f| { + f.hidden_namespaces.contains(namespace) + || f.hidden_action_types.contains(&action.type_id()) + }) { + return None; + } + + Some(Command { + name: humanize_action_name(&name), + action, + }) + }) + .collect(); + + let delegate = + CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle); + + let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + Self { picker } + } +} + +impl EventEmitter for CommandPalette {} + +impl FocusableView for CommandPalette { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for CommandPalette { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + v_stack().w(rems(34.)).child(self.picker.clone()) + } +} pub type CommandPaletteInterceptor = Box Option>; @@ -32,24 +98,26 @@ pub struct CommandInterceptResult { } pub struct CommandPaletteDelegate { - actions: Vec, + command_palette: WeakView, + all_commands: Vec, + commands: Vec, matches: Vec, selected_ix: usize, - focused_view_id: usize, + previous_focus_handle: FocusHandle, } -pub enum Event { - Dismissed, - Confirmed { - window: AnyWindowHandle, - focused_view_id: usize, - action: Box, - }, -} struct Command { name: String, action: Box, - keystrokes: Vec, +} + +impl Clone for Command { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + action: self.action.boxed_clone(), + } + } } /// Hit count for each command in the palette. @@ -58,26 +126,27 @@ struct Command { #[derive(Default)] struct HitCounts(HashMap); -fn toggle_command_palette(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - let focused_view_id = cx.focused_view_id().unwrap_or_else(|| cx.view_id()); - workspace.toggle_modal(cx, |_, cx| { - cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id), cx)) - }); -} - impl CommandPaletteDelegate { - pub fn new(focused_view_id: usize) -> Self { + fn new( + command_palette: WeakView, + commands: Vec, + previous_focus_handle: FocusHandle, + ) -> Self { Self { - actions: Default::default(), + command_palette, + all_commands: commands.clone(), matches: vec![], + commands, selected_ix: 0, - focused_view_id, + previous_focus_handle, } } } impl PickerDelegate for CommandPaletteDelegate { - fn placeholder_text(&self) -> std::sync::Arc { + type ListItem = ListItem; + + fn placeholder_text(&self) -> Arc { "Execute a command...".into() } @@ -98,49 +167,20 @@ impl PickerDelegate for CommandPaletteDelegate { query: String, cx: &mut ViewContext>, ) -> gpui::Task<()> { - let view_id = self.focused_view_id; - let window = cx.window(); - cx.spawn(move |picker, mut cx| async move { - let mut actions = window - .available_actions(view_id, &cx) - .into_iter() - .flatten() - .filter_map(|(name, action, bindings)| { - let filtered = cx.read(|cx| { - if cx.has_global::() { - let filter = cx.global::(); - filter.hidden_namespaces.contains(action.namespace()) - } else { - false - } - }); + let mut commands = self.all_commands.clone(); - if filtered { - None - } else { - Some(Command { - name: humanize_action_name(name), - action, - keystrokes: bindings - .iter() - .map(|binding| binding.keystrokes()) - .last() - .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()), - }) - } - }) - .collect::>(); - let mut actions = cx.read(move |cx| { - let hit_counts = cx.optional_global::(); - actions.sort_by_key(|action| { + cx.spawn(move |picker, mut cx| async move { + cx.read_global::(|hit_counts, _| { + commands.sort_by_key(|action| { ( - Reverse(hit_counts.and_then(|map| map.0.get(&action.name)).cloned()), + Reverse(hit_counts.0.get(&action.name).cloned()), action.name.clone(), ) }); - actions - }); - let candidates = actions + }) + .ok(); + + let candidates = commands .iter() .enumerate() .map(|(ix, command)| StringMatchCandidate { @@ -167,17 +207,17 @@ impl PickerDelegate for CommandPaletteDelegate { true, 10000, &Default::default(), - cx.background(), + cx.background_executor().clone(), ) .await }; - let mut intercept_result = cx.read(|cx| { - if cx.has_global::() { - cx.global::()(&query, cx) - } else { - None - } - }); + + let mut intercept_result = cx + .try_read_global(|interceptor: &CommandPaletteInterceptor, cx| { + (interceptor)(&query, cx) + }) + .flatten(); + if *RELEASE_CHANNEL == ReleaseChannel::Dev { if parse_zed_link(&query).is_some() { intercept_result = Some(CommandInterceptResult { @@ -187,6 +227,7 @@ impl PickerDelegate for CommandPaletteDelegate { }) } } + if let Some(CommandInterceptResult { action, string, @@ -195,29 +236,29 @@ impl PickerDelegate for CommandPaletteDelegate { { if let Some(idx) = matches .iter() - .position(|m| actions[m.candidate_id].action.id() == action.id()) + .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) { matches.remove(idx); } - actions.push(Command { + commands.push(Command { name: string.clone(), action, - keystrokes: vec![], }); matches.insert( 0, StringMatch { - candidate_id: actions.len() - 1, + candidate_id: commands.len() - 1, string, positions, score: 0.0, }, ) } + picker .update(&mut cx, |picker, _| { - let delegate = picker.delegate_mut(); - delegate.actions = actions; + let delegate = &mut picker.delegate; + delegate.commands = commands; delegate.matches = matches; if delegate.matches.is_empty() { delegate.selected_ix = 0; @@ -230,83 +271,60 @@ impl PickerDelegate for CommandPaletteDelegate { }) } - fn dismissed(&mut self, _cx: &mut ViewContext>) {} + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.command_palette + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - if !self.matches.is_empty() { - let window = cx.window(); - let focused_view_id = self.focused_view_id; - let action_ix = self.matches[self.selected_ix].candidate_id; - let command = self.actions.remove(action_ix); - cx.update_default_global(|hit_counts: &mut HitCounts, _| { - *hit_counts.0.entry(command.name).or_default() += 1; - }); - let action = command.action; - - cx.app_context() - .spawn(move |mut cx| async move { - window - .dispatch_action(focused_view_id, action.as_ref(), &mut cx) - .ok_or_else(|| anyhow!("window was closed")) - }) - .detach_and_log_err(cx); + if self.matches.is_empty() { + self.dismissed(cx); + return; } - cx.emit(PickerEvent::Dismiss); + let action_ix = self.matches[self.selected_ix].candidate_id; + let command = self.commands.swap_remove(action_ix); + self.matches.clear(); + self.commands.clear(); + cx.update_global(|hit_counts: &mut HitCounts, _| { + *hit_counts.0.entry(command.name).or_default() += 1; + }); + let action = command.action; + cx.focus(&self.previous_focus_handle); + cx.window_context() + .spawn(move |mut cx| async move { cx.update(|_, cx| cx.dispatch_action(action)) }) + .detach_and_log_err(cx); + self.dismissed(cx); } fn render_match( &self, ix: usize, - mouse_state: &mut MouseState, selected: bool, - cx: &gpui::AppContext, - ) -> AnyElement> { - let mat = &self.matches[ix]; - let command = &self.actions[mat.candidate_id]; - let theme = theme::current(cx); - let style = theme.picker.item.in_state(selected).style_for(mouse_state); - let key_style = &theme.command_palette.key.in_state(selected); - let keystroke_spacing = theme.command_palette.keystroke_spacing; - - Flex::row() - .with_child( - Label::new(mat.string.clone(), style.label.clone()) - .with_highlights(mat.positions.clone()), - ) - .with_children(command.keystrokes.iter().map(|keystroke| { - Flex::row() - .with_children( - [ - (keystroke.ctrl, "^"), - (keystroke.alt, "⌥"), - (keystroke.cmd, "⌘"), - (keystroke.shift, "⇧"), - ] - .into_iter() - .filter_map(|(modifier, label)| { - if modifier { - Some( - Label::new(label, key_style.label.clone()) - .contained() - .with_style(key_style.container), - ) - } else { - None - } - }), - ) - .with_child( - Label::new(keystroke.key.clone(), key_style.label.clone()) - .contained() - .with_style(key_style.container), - ) - .contained() - .with_margin_left(keystroke_spacing) - .flex_float() - })) - .contained() - .with_style(style.container) - .into_any() + cx: &mut ViewContext>, + ) -> Option { + let r#match = self.matches.get(ix)?; + let command = self.commands.get(r#match.candidate_id)?; + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child( + h_stack() + .w_full() + .justify_between() + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) + .children(KeyBinding::for_action_in( + &*command.action, + &self.previous_focus_handle, + cx, + )), + ), + ) } } @@ -338,8 +356,7 @@ impl std::fmt::Debug for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Command") .field("name", &self.name) - .field("keystrokes", &self.keystrokes) - .finish() + .finish_non_exhaustive() } } @@ -349,7 +366,9 @@ mod tests { use super::*; use editor::Editor; - use gpui::{executor::Deterministic, TestAppContext}; + use go_to_line::GoToLine; + use gpui::TestAppContext; + use language::Point; use project::Project; use workspace::{AppState, Workspace}; @@ -370,101 +389,121 @@ mod tests { } #[gpui::test] - async fn test_command_palette(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_command_palette(cx: &mut TestAppContext) { let app_state = init_test(cx); - let project = Project::test(app_state.fs.clone(), [], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let editor = window.add_view(cx, |cx| { - let mut editor = Editor::single_line(None, cx); + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + let editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_text("abc", cx); editor }); workspace.update(cx, |workspace, cx| { - cx.focus(&editor); - workspace.add_item(Box::new(editor.clone()), cx) + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| editor.focus(cx)) }); - workspace.update(cx, |workspace, cx| { - toggle_command_palette(workspace, &Toggle, cx); - }); + cx.simulate_keystrokes("cmd-shift-p"); - let palette = workspace.read_with(cx, |workspace, _| { - workspace.modal::().unwrap() + let palette = workspace.update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() }); - palette - .update(cx, |palette, cx| { - // Fill up palette's command list by running an empty query; - // we only need it to subsequently assert that the palette is initially - // sorted by command's name. - palette.delegate_mut().update_matches("".to_string(), cx) - }) - .await; - palette.update(cx, |palette, _| { + assert!(palette.delegate.commands.len() > 5); let is_sorted = |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); - assert!(is_sorted(&palette.delegate().actions)); + assert!(is_sorted(&palette.delegate.commands)); }); - palette - .update(cx, |palette, cx| { - palette - .delegate_mut() - .update_matches("bcksp".to_string(), cx) - }) - .await; + cx.simulate_input("bcksp"); - palette.update(cx, |palette, cx| { - assert_eq!(palette.delegate().matches[0].string, "editor: backspace"); - palette.confirm(&Default::default(), cx); + palette.update(cx, |palette, _| { + assert_eq!(palette.delegate.matches[0].string, "editor: backspace"); }); - deterministic.run_until_parked(); - editor.read_with(cx, |editor, cx| { - assert_eq!(editor.text(cx), "ab"); + + cx.simulate_keystrokes("enter"); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.active_modal::(cx).is_none()); + assert_eq!(editor.read(cx).text(cx), "ab") }); // Add namespace filter, and redeploy the palette cx.update(|cx| { - cx.update_default_global::(|filter, _| { + cx.set_global(CommandPaletteFilter::default()); + cx.update_global::(|filter, _| { filter.hidden_namespaces.insert("editor"); }) }); - workspace.update(cx, |workspace, cx| { - toggle_command_palette(workspace, &Toggle, cx); + cx.simulate_keystrokes("cmd-shift-p"); + cx.simulate_input("bcksp"); + + let palette = workspace.update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() }); - - // Assert editor command not present - let palette = workspace.read_with(cx, |workspace, _| { - workspace.modal::().unwrap() - }); - - palette - .update(cx, |palette, cx| { - palette - .delegate_mut() - .update_matches("bcksp".to_string(), cx) - }) - .await; - palette.update(cx, |palette, _| { - assert!(palette.delegate().matches.is_empty()) + assert!(palette.delegate.matches.is_empty()) + }); + } + + #[gpui::test] + async fn test_go_to_line(cx: &mut TestAppContext) { + let app_state = init_test(cx); + let project = Project::test(app_state.fs.clone(), [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + cx.simulate_keystrokes("cmd-n"); + + let editor = workspace.update(cx, |workspace, cx| { + workspace.active_item_as::(cx).unwrap() + }); + editor.update(cx, |editor, cx| editor.set_text("1\n2\n3\n4\n5\n6\n", cx)); + + cx.simulate_keystrokes("cmd-shift-p"); + cx.simulate_input("go to line: Toggle"); + cx.simulate_keystrokes("enter"); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.active_modal::(cx).is_some()) + }); + + cx.simulate_keystrokes("3 enter"); + + editor.update(cx, |editor, cx| { + assert!(editor.focus_handle(cx).is_focused(cx)); + assert_eq!( + editor.selections.last::(cx).range().start, + Point::new(2, 0) + ); }); } fn init_test(cx: &mut TestAppContext) -> Arc { cx.update(|cx| { let app_state = AppState::test(cx); - theme::init((), cx); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); editor::init(cx); + menu::init(); + go_to_line::init(cx); workspace::init(app_state.clone(), cx); init(cx); Project::init_settings(cx); + settings::load_default_keymap(cx); app_state }) } diff --git a/crates/command_palette2/Cargo.toml b/crates/command_palette2/Cargo.toml deleted file mode 100644 index 34438cce14..0000000000 --- a/crates/command_palette2/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "command_palette2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/command_palette.rs" -doctest = false - -[dependencies] -collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } -fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -gpui = { package = "gpui2", path = "../gpui2" } -picker = { package = "picker2", path = "../picker2" } -project = { package = "project2", path = "../project2" } -settings = { package = "settings2", path = "../settings2" } -ui = { package = "ui2", path = "../ui2" } -util = { path = "../util" } -theme = { package = "theme2", path = "../theme2" } -workspace = { package="workspace2", path = "../workspace2" } -zed_actions = { package = "zed_actions2", path = "../zed_actions2" } -anyhow.workspace = true -serde.workspace = true -[dev-dependencies] -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } -language = { package="language2", path = "../language2", features = ["test-support"] } -project = { package="project2", path = "../project2", features = ["test-support"] } -menu = { package = "menu2", path = "../menu2" } -go_to_line = { package = "go_to_line2", path = "../go_to_line2" } -serde_json.workspace = true -workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] } -ctor.workspace = true -env_logger.workspace = true diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs deleted file mode 100644 index b7a1dbfd3d..0000000000 --- a/crates/command_palette2/src/command_palette.rs +++ /dev/null @@ -1,510 +0,0 @@ -use std::{ - cmp::{self, Reverse}, - sync::Arc, -}; - -use collections::{CommandPaletteFilter, HashMap}; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, - ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, -}; -use picker::{Picker, PickerDelegate}; - -use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing}; -use util::{ - channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, - ResultExt, -}; -use workspace::{ModalView, Workspace}; -use zed_actions::OpenZedURL; - -actions!(command_palette, [Toggle]); - -pub fn init(cx: &mut AppContext) { - cx.set_global(HitCounts::default()); - cx.set_global(CommandPaletteFilter::default()); - cx.observe_new_views(CommandPalette::register).detach(); -} - -impl ModalView for CommandPalette {} - -pub struct CommandPalette { - picker: View>, -} - -impl CommandPalette { - fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, _: &Toggle, cx| { - let Some(previous_focus_handle) = cx.focused() else { - return; - }; - workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx)); - }); - } - - fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext) -> Self { - let filter = cx.try_global::(); - - let commands = cx - .available_actions() - .into_iter() - .filter_map(|action| { - let name = action.name(); - let namespace = name.split("::").next().unwrap_or("malformed action name"); - if filter.is_some_and(|f| { - f.hidden_namespaces.contains(namespace) - || f.hidden_action_types.contains(&action.type_id()) - }) { - return None; - } - - Some(Command { - name: humanize_action_name(&name), - action, - }) - }) - .collect(); - - let delegate = - CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle); - - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); - Self { picker } - } -} - -impl EventEmitter for CommandPalette {} - -impl FocusableView for CommandPalette { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for CommandPalette { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) - } -} - -pub type CommandPaletteInterceptor = - Box Option>; - -pub struct CommandInterceptResult { - pub action: Box, - pub string: String, - pub positions: Vec, -} - -pub struct CommandPaletteDelegate { - command_palette: WeakView, - all_commands: Vec, - commands: Vec, - matches: Vec, - selected_ix: usize, - previous_focus_handle: FocusHandle, -} - -struct Command { - name: String, - action: Box, -} - -impl Clone for Command { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - action: self.action.boxed_clone(), - } - } -} - -/// Hit count for each command in the palette. -/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because -/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it. -#[derive(Default)] -struct HitCounts(HashMap); - -impl CommandPaletteDelegate { - fn new( - command_palette: WeakView, - commands: Vec, - previous_focus_handle: FocusHandle, - ) -> Self { - Self { - command_palette, - all_commands: commands.clone(), - matches: vec![], - commands, - selected_ix: 0, - previous_focus_handle, - } - } -} - -impl PickerDelegate for CommandPaletteDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self) -> Arc { - "Execute a command...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_ix - } - - fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { - self.selected_ix = ix; - } - - fn update_matches( - &mut self, - query: String, - cx: &mut ViewContext>, - ) -> gpui::Task<()> { - let mut commands = self.all_commands.clone(); - - cx.spawn(move |picker, mut cx| async move { - cx.read_global::(|hit_counts, _| { - commands.sort_by_key(|action| { - ( - Reverse(hit_counts.0.get(&action.name).cloned()), - action.name.clone(), - ) - }); - }) - .ok(); - - let candidates = commands - .iter() - .enumerate() - .map(|(ix, command)| StringMatchCandidate { - id: ix, - string: command.name.to_string(), - char_bag: command.name.chars().collect(), - }) - .collect::>(); - let mut matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - fuzzy::match_strings( - &candidates, - &query, - true, - 10000, - &Default::default(), - cx.background_executor().clone(), - ) - .await - }; - - let mut intercept_result = cx - .try_read_global(|interceptor: &CommandPaletteInterceptor, cx| { - (interceptor)(&query, cx) - }) - .flatten(); - - if *RELEASE_CHANNEL == ReleaseChannel::Dev { - if parse_zed_link(&query).is_some() { - intercept_result = Some(CommandInterceptResult { - action: OpenZedURL { url: query.clone() }.boxed_clone(), - string: query.clone(), - positions: vec![], - }) - } - } - - if let Some(CommandInterceptResult { - action, - string, - positions, - }) = intercept_result - { - if let Some(idx) = matches - .iter() - .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) - { - matches.remove(idx); - } - commands.push(Command { - name: string.clone(), - action, - }); - matches.insert( - 0, - StringMatch { - candidate_id: commands.len() - 1, - string, - positions, - score: 0.0, - }, - ) - } - - picker - .update(&mut cx, |picker, _| { - let delegate = &mut picker.delegate; - delegate.commands = commands; - delegate.matches = matches; - if delegate.matches.is_empty() { - delegate.selected_ix = 0; - } else { - delegate.selected_ix = - cmp::min(delegate.selected_ix, delegate.matches.len() - 1); - } - }) - .log_err(); - }) - } - - fn dismissed(&mut self, cx: &mut ViewContext>) { - self.command_palette - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - if self.matches.is_empty() { - self.dismissed(cx); - return; - } - let action_ix = self.matches[self.selected_ix].candidate_id; - let command = self.commands.swap_remove(action_ix); - self.matches.clear(); - self.commands.clear(); - cx.update_global(|hit_counts: &mut HitCounts, _| { - *hit_counts.0.entry(command.name).or_default() += 1; - }); - let action = command.action; - cx.focus(&self.previous_focus_handle); - cx.window_context() - .spawn(move |mut cx| async move { cx.update(|_, cx| cx.dispatch_action(action)) }) - .detach_and_log_err(cx); - self.dismissed(cx); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - cx: &mut ViewContext>, - ) -> Option { - let r#match = self.matches.get(ix)?; - let command = self.commands.get(r#match.candidate_id)?; - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .selected(selected) - .child( - h_stack() - .w_full() - .justify_between() - .child(HighlightedLabel::new( - command.name.clone(), - r#match.positions.clone(), - )) - .children(KeyBinding::for_action_in( - &*command.action, - &self.previous_focus_handle, - cx, - )), - ), - ) - } -} - -fn humanize_action_name(name: &str) -> String { - let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); - let mut result = String::with_capacity(capacity); - for char in name.chars() { - if char == ':' { - if result.ends_with(':') { - result.push(' '); - } else { - result.push(':'); - } - } else if char == '_' { - result.push(' '); - } else if char.is_uppercase() { - if !result.ends_with(' ') { - result.push(' '); - } - result.extend(char.to_lowercase()); - } else { - result.push(char); - } - } - result -} - -impl std::fmt::Debug for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Command") - .field("name", &self.name) - .finish_non_exhaustive() - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use super::*; - use editor::Editor; - use go_to_line::GoToLine; - use gpui::TestAppContext; - use language::Point; - use project::Project; - use workspace::{AppState, Workspace}; - - #[test] - fn test_humanize_action_name() { - assert_eq!( - humanize_action_name("editor::GoToDefinition"), - "editor: go to definition" - ); - assert_eq!( - humanize_action_name("editor::Backspace"), - "editor: backspace" - ); - assert_eq!( - humanize_action_name("go_to_line::Deploy"), - "go to line: deploy" - ); - } - - #[gpui::test] - async fn test_command_palette(cx: &mut TestAppContext) { - let app_state = init_test(cx); - let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); - - let editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_text("abc", cx); - editor - }); - - workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(editor.clone()), cx); - editor.update(cx, |editor, cx| editor.focus(cx)) - }); - - cx.simulate_keystrokes("cmd-shift-p"); - - let palette = workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }); - - palette.update(cx, |palette, _| { - assert!(palette.delegate.commands.len() > 5); - let is_sorted = - |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); - assert!(is_sorted(&palette.delegate.commands)); - }); - - cx.simulate_input("bcksp"); - - palette.update(cx, |palette, _| { - assert_eq!(palette.delegate.matches[0].string, "editor: backspace"); - }); - - cx.simulate_keystrokes("enter"); - - workspace.update(cx, |workspace, cx| { - assert!(workspace.active_modal::(cx).is_none()); - assert_eq!(editor.read(cx).text(cx), "ab") - }); - - // Add namespace filter, and redeploy the palette - cx.update(|cx| { - cx.set_global(CommandPaletteFilter::default()); - cx.update_global::(|filter, _| { - filter.hidden_namespaces.insert("editor"); - }) - }); - - cx.simulate_keystrokes("cmd-shift-p"); - cx.simulate_input("bcksp"); - - let palette = workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }); - palette.update(cx, |palette, _| { - assert!(palette.delegate.matches.is_empty()) - }); - } - - #[gpui::test] - async fn test_go_to_line(cx: &mut TestAppContext) { - let app_state = init_test(cx); - let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); - - cx.simulate_keystrokes("cmd-n"); - - let editor = workspace.update(cx, |workspace, cx| { - workspace.active_item_as::(cx).unwrap() - }); - editor.update(cx, |editor, cx| editor.set_text("1\n2\n3\n4\n5\n6\n", cx)); - - cx.simulate_keystrokes("cmd-shift-p"); - cx.simulate_input("go to line: Toggle"); - cx.simulate_keystrokes("enter"); - - workspace.update(cx, |workspace, cx| { - assert!(workspace.active_modal::(cx).is_some()) - }); - - cx.simulate_keystrokes("3 enter"); - - editor.update(cx, |editor, cx| { - assert!(editor.focus_handle(cx).is_focused(cx)); - assert_eq!( - editor.selections.last::(cx).range().start, - Point::new(2, 0) - ); - }); - } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let app_state = AppState::test(cx); - theme::init(theme::LoadThemes::JustBase, cx); - language::init(cx); - editor::init(cx); - menu::init(); - go_to_line::init(cx); - workspace::init(app_state.clone(), cx); - init(cx); - Project::init_settings(cx); - settings::load_default_keymap(cx); - app_state - }) - } -} diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 0f9d108831..35fa5598ad 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -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 diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 9ff3c04de8..77e6a7673f 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -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::(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, - workspace: WeakViewHandle, - editor: ViewHandle, + project: Model, + workspace: WeakView, + focus_handle: FocusHandle, + editor: View, summary: DiagnosticSummary, - excerpts: ModelHandle, + excerpts: Model, path_states: Vec, paths_to_update: HashMap>, current_diagnostics: HashMap>, @@ -89,71 +88,38 @@ struct DiagnosticGroupState { block_count: usize, } -impl Entity for ProjectDiagnosticsEditor { - type Event = Event; -} +impl EventEmitter for ProjectDiagnosticsEditor {} -impl View for ProjectDiagnosticsEditor { - fn ui_name() -> &'static str { - "ProjectDiagnosticsEditor" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - 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) -> 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) { - 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::>(), - "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::>()) - ).collect::>(), - "current_diagnostics": self.current_diagnostics.iter().map(|(server_id, paths)| - (server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::>()) - ).collect::>(), - "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::>(), - }) - ).collect::>(), - }) + 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.register_action(Self::deploy); + } + fn new( - project_handle: ModelHandle, - workspace: WeakViewHandle, + project_handle: Model, + workspace: WeakView, cx: &mut ViewContext, ) -> 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::(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::(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) { + 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, @@ -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, - buffer: ModelHandle, + buffer: Model, cx: &mut ViewContext, ) { 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( - &self, - _detail: Option, - style: &theme::Tab, - cx: &AppContext, - ) -> AnyElement { - 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.editor - .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| editor.deactivated(cx)); } fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { @@ -660,10 +635,82 @@ impl Item for ProjectDiagnosticsEditor { .update(cx, |editor, cx| editor.navigate(data, cx)) } - fn tab_tooltip_text(&self, _: &AppContext) -> Option> { + fn tab_tooltip_text(&self, _: &AppContext) -> Option { Some("Project Diagnostics".into()) } + fn tab_content(&self, _detail: Option, 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.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn clone_on_split( + &self, + _workspace_id: workspace::WorkspaceId, + cx: &mut ViewContext, + ) -> Option> + 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, - cx: &mut ViewContext, - ) -> Task> { + fn save(&mut self, project: Model, cx: &mut ViewContext) -> Task> { self.editor.save(project, cx) } - fn reload( - &mut self, - project: ModelHandle, - cx: &mut ViewContext, - ) -> Task> { - self.editor.reload(project, cx) - } - fn save_as( &mut self, - _: ModelHandle, + _: Model, _: PathBuf, _: &mut ViewContext, ) -> Task> { 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.editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - } - - fn clone_on_split( - &self, - _workspace_id: workspace::WorkspaceId, - cx: &mut ViewContext, - ) -> Option - where - Self: Sized, - { - Some(ProjectDiagnosticsEditor::new( - self.project.clone(), - self.workspace.clone(), - cx, - )) + fn reload(&mut self, project: Model, cx: &mut ViewContext) -> Task> { + self.editor.reload(project, cx) } fn act_as_type<'a>( &'a self, type_id: TypeId, - self_handle: &'a ViewHandle, + self_handle: &'a View, _: &'a AppContext, - ) -> Option<&AnyViewHandle> { + ) -> Option { if type_id == TypeId::of::() { - Some(self_handle) + Some(self_handle.to_any()) } else if type_id == TypeId::of::() { - Some(&self.editor) + Some(self.editor.to_any()) } else { None } } - fn deactivated(&mut self, cx: &mut ViewContext) { - 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> { 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.editor + .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("diagnostics") } fn deserialize( - project: ModelHandle, - workspace: WeakViewHandle, + project: Model, + workspace: WeakView, _workspace_id: workspace::WorkspaceId, _item_id: workspace::ItemId, cx: &mut ViewContext, - ) -> Task>> { - Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx)))) + ) -> Task>> { + 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::(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( - summary: &DiagnosticSummary, - text_style: &TextStyle, - theme: &theme::ProjectDiagnostics, -) -> AnyElement { - 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( lhs: &DiagnosticEntry, rhs: &DiagnosticEntry, @@ -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, cx: &mut WindowContext) -> Vec<(u32, String)> { + fn editor_blocks(editor: &View, 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() } } }; diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 86d8d01db1..9f7be0c04f 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -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>, - workspace: WeakViewHandle, + active_editor: Option>, + workspace: WeakView, current_diagnostic: Option, in_progress_checks: HashSet, _observe_active_editor: Option, } -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) -> 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) { - 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) { + 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, cx: &mut ViewContext) { + fn update(&mut self, editor: View, cx: &mut ViewContext) { let editor = editor.read(cx); let buffer = editor.buffer().read(cx); let cursor_position = editor.selections.newest::(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) -> AnyElement { - 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::(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::( - 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::(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 for DiagnosticIndicator {} impl StatusItemView for DiagnosticIndicator { fn set_active_pane_item( diff --git a/crates/diagnostics/src/project_diagnostics_settings.rs b/crates/diagnostics/src/project_diagnostics_settings.rs index 1592d3c7f0..f762d2b1e6 100644 --- a/crates/diagnostics/src/project_diagnostics_settings.rs +++ b/crates/diagnostics/src/project_diagnostics_settings.rs @@ -11,14 +11,14 @@ pub struct ProjectDiagnosticsSettingsContent { include_warnings: Option, } -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 where Self: Sized, diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 421571eede..897e2ccf40 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -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>, + editor: Option>, } -impl Entity for ToolbarControls { - type Event = (); -} - -impl View for ToolbarControls { - fn ui_name() -> &'static str { - "ToolbarControls" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +impl Render for ToolbarControls { + fn render(&mut self, cx: &mut ViewContext) -> 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 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::() { 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), ->( - index: usize, - icon: &'static str, - toggled: bool, - tooltip: (String, Option>), - cx: &mut ViewContext, - on_click: F, -) -> AnyElement { - enum Button {} - - let theme = theme::current(cx); - let (tooltip_text, action) = tooltip; - - MouseEventHandler::new::(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::