diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d8ee49866b..f63d44ee67 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -990,6 +990,15 @@ impl Editor { Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx) } + pub fn multi_line( + field_editor_style: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) + } + pub fn auto_height( max_lines: usize, field_editor_style: Option>, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bf6cb57adb..3ce8894238 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -25,6 +25,7 @@ pub struct Theme { pub command_palette: CommandPalette, pub picker: Picker, pub editor: Editor, + // pub feedback_box: Editor, pub search: Search, pub project_diagnostics: ProjectDiagnostics, pub breadcrumbs: ContainedText, @@ -119,6 +120,22 @@ pub struct ContactList { pub calling_indicator: ContainedText, } +// TODO FEEDBACK: Remove or use this +// #[derive(Deserialize, Default)] +// pub struct FeedbackPopover { +// #[serde(flatten)] +// pub container: ContainerStyle, +// pub height: f32, +// pub width: f32, +// pub invite_row_height: f32, +// pub invite_row: Interactive, +// } + +// #[derive(Deserialize, Default)] +// pub struct FeedbackBox { +// pub feedback_editor: FieldEditor, +// } + #[derive(Deserialize, Default)] pub struct ProjectRow { #[serde(flatten)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 618c650e02..5456ecaf60 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -95,6 +95,11 @@ pub struct DeployNewMenu { position: Vector2F, } +#[derive(Clone, PartialEq)] +pub struct DeployFeedbackModal { + position: Vector2F, +} + impl_actions!(pane, [GoBack, GoForward, ActivateItem]); impl_internal_actions!( pane, @@ -104,6 +109,7 @@ impl_internal_actions!( DeployNewMenu, DeployDockMenu, MoveItem, + DeployFeedbackModal ] ); diff --git a/crates/zed/src/feedback.rs b/crates/zed/src/feedback.rs deleted file mode 100644 index 55597312ae..0000000000 --- a/crates/zed/src/feedback.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::OpenBrowser; -use gpui::{ - elements::{MouseEventHandler, Text}, - platform::CursorStyle, - Element, Entity, MouseButton, RenderContext, View, -}; -use settings::Settings; -use workspace::{item::ItemHandle, StatusItemView}; - -pub const NEW_ISSUE_URL: &str = "https://github.com/zed-industries/feedback/issues/new/choose"; - -pub struct FeedbackLink; - -impl Entity for FeedbackLink { - type Event = (); -} - -impl View for FeedbackLink { - fn ui_name() -> &'static str { - "FeedbackLink" - } - - fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> gpui::ElementBox { - MouseEventHandler::::new(0, cx, |state, cx| { - let theme = &cx.global::().theme; - let theme = &theme.workspace.status_bar.feedback; - Text::new( - "Give Feedback".to_string(), - theme.style_for(state, false).clone(), - ) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(OpenBrowser { - url: NEW_ISSUE_URL.into(), - }) - }) - .boxed() - } -} - -impl StatusItemView for FeedbackLink { - fn set_active_pane_item( - &mut self, - _: Option<&dyn ItemHandle>, - _: &mut gpui::ViewContext, - ) { - } -} diff --git a/crates/zed/src/feedback_popover.rs b/crates/zed/src/feedback_popover.rs new file mode 100644 index 0000000000..52397562f9 --- /dev/null +++ b/crates/zed/src/feedback_popover.rs @@ -0,0 +1,326 @@ +use std::{ops::Range, sync::Arc}; + +use anyhow::bail; +use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; +use editor::Editor; +use futures::AsyncReadExt; +use gpui::{ + actions, + elements::{ + AnchorCorner, ChildView, Flex, MouseEventHandler, Overlay, OverlayFitMode, ParentElement, + Stack, Text, + }, + serde_json, CursorStyle, Element, ElementBox, Entity, MouseButton, MutableAppContext, + RenderContext, View, ViewContext, ViewHandle, +}; +use isahc::Request; +use lazy_static::lazy_static; +use serde::Serialize; +use settings::Settings; +use workspace::{item::ItemHandle, StatusItemView}; + +use crate::{feedback_popover, system_specs::SystemSpecs}; + +/* + TODO FEEDBACK + + Next steps from Mikayla: + 1: Find the bottom bar height and maybe guess some feedback button widths? + Basically, just use to position the modal + 2: Look at ContactList::render() and ContactPopover::render() for clues on how + to make the modal look nice. Copy the theme values from the contact list styles + + Now + Fix all layout issues, theming, buttons, etc + Make multi-line editor without line numbers + Some sort of feedback when something fails or succeeds + Naming of all UI stuff and separation out into files (follow a convention already in place) + Disable submit button when text length is 0 + Should we store staff boolean? + Put behind experiments flag + Move to separate crate + Render a character counter + All warnings + Remove all comments + Later + If a character limit is imposed, switch submit button over to a "GitHub Issue" button + Should editor by treated as a markdown file + Limit characters? + Disable submit button when text length is GTE to character limit + + Pay for AirTable + Add AirTable to system architecture diagram in Figma +*/ + +lazy_static! { + pub static ref ZED_SERVER_URL: String = + std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string()); +} + +const FEEDBACK_CHAR_COUNT_RANGE: Range = Range { + start: 5, + end: 1000, +}; + +actions!(feedback, [ToggleFeedbackPopover, SubmitFeedback]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(FeedbackButton::toggle_feedback); + cx.add_action(FeedbackPopover::submit_feedback); +} + +pub struct FeedbackButton { + feedback_popover: Option>, +} + +impl FeedbackButton { + pub fn new() -> Self { + Self { + feedback_popover: None, + } + } + + pub fn toggle_feedback(&mut self, _: &ToggleFeedbackPopover, cx: &mut ViewContext) { + match self.feedback_popover.take() { + Some(_) => {} + None => { + let popover_view = cx.add_view(|_cx| FeedbackPopover::new(_cx)); + self.feedback_popover = Some(popover_view.clone()); + } + } + + cx.notify(); + } +} + +impl Entity for FeedbackButton { + type Event = (); +} + +impl View for FeedbackButton { + fn ui_name() -> &'static str { + "FeedbackButton" + } + + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, cx| { + let theme = &cx.global::().theme; + let theme = &theme.workspace.status_bar.feedback; + + Text::new( + "Give Feedback".to_string(), + theme + .style_for(state, self.feedback_popover.is_some()) + .clone(), + ) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(ToggleFeedbackPopover) + }) + .boxed(), + ) + .with_children(self.feedback_popover.as_ref().map(|popover| { + Overlay::new( + ChildView::new(popover, cx) + .contained() + // .with_height(theme.contact_list.user_query_editor_height) + // .with_margin_top(-50.0) + // .with_margin_left(titlebar.toggle_contacts_button.default.button_width) + // .with_margin_right(-titlebar.toggle_contacts_button.default.button_width) + .boxed(), + ) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::TopLeft) + .with_z_index(999) + .boxed() + })) + .boxed() + } +} + +impl StatusItemView for FeedbackButton { + fn set_active_pane_item( + &mut self, + _: Option<&dyn ItemHandle>, + _: &mut gpui::ViewContext, + ) { + // N/A + } +} + +pub struct FeedbackPopover { + feedback_editor: ViewHandle, + // _subscriptions: Vec, +} + +impl Entity for FeedbackPopover { + type Event = (); +} + +#[derive(Serialize)] +struct FeedbackRequestBody<'a> { + feedback_text: &'a str, + metrics_id: Option>, + system_specs: SystemSpecs, + token: &'a str, +} + +impl FeedbackPopover { + pub fn new(cx: &mut ViewContext) -> Self { + let feedback_editor = cx.add_view(|cx| { + let editor = Editor::multi_line( + Some(Arc::new(|theme| { + theme.contact_list.user_query_editor.clone() + })), + cx, + ); + editor + }); + + cx.focus(&feedback_editor); + + cx.subscribe(&feedback_editor, |this, _, event, cx| { + if let editor::Event::BufferEdited = event { + let buffer_len = this.feedback_editor.read(cx).buffer().read(cx).len(cx); + let feedback_chars_remaining = FEEDBACK_CHAR_COUNT_RANGE.end - buffer_len; + dbg!(feedback_chars_remaining); + } + }) + .detach(); + + // let active_call = ActiveCall::global(cx); + // let mut subscriptions = Vec::new(); + // subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); + // subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); + let this = Self { + feedback_editor, // _subscriptions: subscriptions, + }; + // this.update_entries(cx); + this + } + + fn submit_feedback(&mut self, _: &SubmitFeedback, cx: &mut ViewContext<'_, Self>) { + let feedback_text = self.feedback_editor.read(cx).text(cx); + let http_client = cx.global::>().clone(); + let system_specs = SystemSpecs::new(cx); + let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL); + + cx.spawn(|this, async_cx| { + async move { + // TODO FEEDBACK: Use or remove + // this.read_with(&async_cx, |this, cx| { + // // Now we have a &self and a &AppContext + // }); + + let metrics_id = None; + + let request = FeedbackRequestBody { + feedback_text: &feedback_text, + metrics_id, + system_specs, + token: ZED_SECRET_CLIENT_TOKEN, + }; + + let json_bytes = serde_json::to_vec(&request)?; + + let request = Request::post(feedback_endpoint) + .header("content-type", "application/json") + .body(json_bytes.into())?; + + let mut response = http_client.send(request).await?; + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + let response_status = response.status(); + + dbg!(response_status); + + if !response_status.is_success() { + // TODO FEEDBACK: Do some sort of error reporting here for if store fails + bail!("Error") + } + + // TODO FEEDBACK: Use or remove + // Will need to handle error cases + // async_cx.update(|cx| { + // this.update(cx, |this, cx| { + // this.handle_error(error); + // cx.notify(); + // cx.dispatch_action(ShowErrorPopover); + // this.error_text = "Embedding failed" + // }) + // }); + + Ok(()) + } + }) + .detach(); + } +} + +impl View for FeedbackPopover { + fn ui_name() -> &'static str { + "FeedbackPopover" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + enum SubmitFeedback {} + + let theme = cx.global::().theme.clone(); + let status_bar_height = theme.workspace.status_bar.height; + let submit_feedback_text_button_height = 20.0; + + // I'd like to just define: + + // 1. Overall popover width x height dimensions + // 2. Submit Feedback button height dimensions + // 3. Allow editor to dynamically fill in the remaining space + + Flex::column() + .with_child( + Flex::row() + .with_child( + ChildView::new(self.feedback_editor.clone(), cx) + .contained() + .with_style(theme.contact_list.user_query_editor.container) + .flex(1., true) + .boxed(), + ) + .constrained() + .with_width(theme.contacts_popover.width) + .with_height(theme.contacts_popover.height - submit_feedback_text_button_height) + .boxed(), + ) + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let theme = &theme.workspace.status_bar.feedback; + + Text::new( + "Submit Feedback".to_string(), + theme.style_for(state, true).clone(), + ) + .constrained() + .with_height(submit_feedback_text_button_height) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(feedback_popover::SubmitFeedback) + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(feedback_popover::ToggleFeedbackPopover) + }) + .boxed(), + ) + .contained() + .with_style(theme.contacts_popover.container) + .constrained() + .with_width(theme.contacts_popover.width + 200.0) + .with_height(theme.contacts_popover.height) + .boxed() + } +} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5ac7cb36b6..90527af555 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -41,7 +41,7 @@ use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{ self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace, }; -use zed::{self, build_window_options, initialize_workspace, languages, menus}; +use zed::{self, build_window_options, feedback_popover, initialize_workspace, languages, menus}; fn main() { let http = http::client(); @@ -108,6 +108,9 @@ fn main() { watch_settings_file(default_settings, settings_file_content, themes.clone(), cx); watch_keymap_file(keymap_file, cx); + cx.set_global(http.clone()); + + feedback_popover::init(cx); context_menu::init(cx); project::Project::init(&client); client::init(client.clone(), cx); diff --git a/crates/zed/src/system_specs.rs b/crates/zed/src/system_specs.rs index b6c2c0fcba..fc55f76f35 100644 --- a/crates/zed/src/system_specs.rs +++ b/crates/zed/src/system_specs.rs @@ -2,9 +2,11 @@ use std::{env, fmt::Display}; use gpui::AppContext; use human_bytes::human_bytes; +use serde::Serialize; use sysinfo::{System, SystemExt}; use util::channel::ReleaseChannel; +#[derive(Debug, Serialize)] pub struct SystemSpecs { app_version: &'static str, release_channel: &'static str, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index e6d50f43af..527da286d2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,4 +1,4 @@ -mod feedback; +pub mod feedback_popover; pub mod languages; pub mod menus; pub mod system_specs; @@ -369,7 +369,7 @@ pub fn initialize_workspace( let activity_indicator = activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); - let feedback_link = cx.add_view(|_| feedback::FeedbackLink); + let feedback_link = cx.add_view(|_| feedback_popover::FeedbackButton::new()); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx);