diff --git a/Cargo.lock b/Cargo.lock index b48fd4dc3d..1f15c9c8e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3223,6 +3223,39 @@ dependencies = [ "workspace", ] +[[package]] +name = "feedback2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "db2", + "editor2", + "futures 0.3.28", + "gpui2", + "human_bytes", + "isahc", + "language2", + "lazy_static", + "log", + "menu2", + "postage", + "project2", + "regex", + "search2", + "serde", + "serde_derive", + "settings2", + "smallvec", + "sysinfo", + "theme2", + "tree-sitter-markdown", + "ui2", + "urlencoding", + "util", + "workspace2", +] + [[package]] name = "file-per-thread-logger" version = "0.1.6" @@ -11943,6 +11976,7 @@ dependencies = [ "editor2", "env_logger 0.9.3", "feature_flags2", + "feedback2", "file_finder2", "fs2", "fsevent", diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 8f9d22e6c9..796fc68fe6 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1717,6 +1717,11 @@ impl Editor { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, Self::handle_focus).detach(); cx.on_blur(&focus_handle, Self::handle_blur).detach(); + cx.on_release(|this, cx| { + //todo!() + //cx.emit_global(EditorReleased(self.handle.clone())); + }) + .detach(); let mut this = Self { handle: cx.view().downgrade(), @@ -8191,6 +8196,17 @@ impl Editor { self.buffer.read(cx).read(cx).text() } + pub fn text_option(&self, cx: &AppContext) -> Option { + let text = self.buffer.read(cx).read(cx).text(); + let text = text.trim(); + + if text.is_empty() { + return None; + } + + Some(text.to_string()) + } + pub fn set_text(&mut self, text: impl Into>, cx: &mut ViewContext) { self.transact(cx, |this, cx| { this.buffer @@ -9252,14 +9268,6 @@ pub struct EditorFocused(pub View); pub struct EditorBlurred(pub View); pub struct EditorReleased(pub WeakView); -// impl Entity for Editor { -// type Event = Event; - -// fn release(&mut self, cx: &mut AppContext) { -// cx.emit_global(EditorReleased(self.handle.clone())); -// } -// } -// impl EventEmitter for Editor {} impl FocusableView for Editor { diff --git a/crates/feedback2/Cargo.toml b/crates/feedback2/Cargo.toml new file mode 100644 index 0000000000..560c5a307f --- /dev/null +++ b/crates/feedback2/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "feedback2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/feedback2.rs" + +[features] +test-support = [] + +[dependencies] +client = { package = "client2", path = "../client2" } +db = { package = "db2", path = "../db2" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } +project = { package = "project2", path = "../project2" } +regex.workspace = true +search = { package = "search2", path = "../search2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } +util = { path = "../util" } +workspace = { package = "workspace2", path = "../workspace2"} + +log.workspace = true +futures.workspace = true +anyhow.workspace = true +smallvec.workspace = true +human_bytes = "0.4.1" +isahc.workspace = true +lazy_static.workspace = true +postage.workspace = true +serde.workspace = true +serde_derive.workspace = true +sysinfo.workspace = true +tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } +urlencoding = "2.1.2" + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/feedback2/src/deploy_feedback_button.rs b/crates/feedback2/src/deploy_feedback_button.rs new file mode 100644 index 0000000000..e5884cf9b1 --- /dev/null +++ b/crates/feedback2/src/deploy_feedback_button.rs @@ -0,0 +1,50 @@ +use gpui::{AnyElement, Render, ViewContext, WeakView}; +use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip}; +use workspace::{item::ItemHandle, StatusItemView, Workspace}; + +use crate::{feedback_modal::FeedbackModal, GiveFeedback}; + +pub struct DeployFeedbackButton { + workspace: WeakView, +} + +impl DeployFeedbackButton { + pub fn new(workspace: &Workspace) -> Self { + DeployFeedbackButton { + workspace: workspace.weak_handle(), + } + } +} + +impl Render for DeployFeedbackButton { + type Element = AnyElement; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let is_open = self + .workspace + .upgrade() + .and_then(|workspace| { + workspace.update(cx, |workspace, cx| { + workspace.active_modal::(cx) + }) + }) + .is_some(); + IconButton::new("give-feedback", Icon::Envelope) + .style(ui::ButtonStyle::Subtle) + .selected(is_open) + .tooltip(|cx| Tooltip::text("Give Feedback", cx)) + .on_click(|_, cx| { + cx.dispatch_action(Box::new(GiveFeedback)); + }) + .into_any_element() + } +} + +impl StatusItemView for DeployFeedbackButton { + fn set_active_pane_item( + &mut self, + _item: Option<&dyn ItemHandle>, + _cx: &mut ViewContext, + ) { + } +} diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs new file mode 100644 index 0000000000..1a1bd93526 --- /dev/null +++ b/crates/feedback2/src/feedback2.rs @@ -0,0 +1,58 @@ +use gpui::{actions, AppContext, ClipboardItem, PromptLevel}; +use system_specs::SystemSpecs; +use workspace::Workspace; + +pub mod deploy_feedback_button; +pub mod feedback_modal; + +actions!(GiveFeedback, SubmitFeedback); + +mod system_specs; + +actions!( + CopySystemSpecsIntoClipboard, + FileBugReport, + RequestFeature, + OpenZedCommunityRepo +); + +pub fn init(cx: &mut AppContext) { + // TODO: a way to combine these two into one? + cx.observe_new_views(feedback_modal::FeedbackModal::register) + .detach(); + + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace + .register_action(|_, _: &CopySystemSpecsIntoClipboard, cx| { + let specs = SystemSpecs::new(&cx).to_string(); + + let prompt = cx.prompt( + PromptLevel::Info, + &format!("Copied into clipboard:\n\n{specs}"), + &["OK"], + ); + cx.spawn(|_, _cx| async move { + prompt.await.ok(); + }) + .detach(); + let item = ClipboardItem::new(specs.clone()); + cx.write_to_clipboard(item); + }) + .register_action(|_, _: &RequestFeature, cx| { + let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml"; + cx.open_url(url); + }) + .register_action(move |_, _: &FileBugReport, cx| { + let url = format!( + "https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}", + urlencoding::encode(&SystemSpecs::new(&cx).to_string()) + ); + cx.open_url(&url); + }) + .register_action(move |_, _: &OpenZedCommunityRepo, cx| { + let url = "https://github.com/zed-industries/community"; + cx.open_url(&url); + }); + }) + .detach(); +} diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs new file mode 100644 index 0000000000..68fbcfb3a3 --- /dev/null +++ b/crates/feedback2/src/feedback_modal.rs @@ -0,0 +1,382 @@ +use std::{ops::RangeInclusive, sync::Arc}; + +use anyhow::bail; +use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; +use db::kvp::KEY_VALUE_STORE; +use editor::{Editor, EditorEvent}; +use futures::AsyncReadExt; +use gpui::{ + div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, + FocusableView, Model, PromptLevel, Render, Task, View, ViewContext, +}; +use isahc::Request; +use language::Buffer; +use project::Project; +use regex::Regex; +use serde_derive::Serialize; +use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip}; +use util::ResultExt; +use workspace::Workspace; + +use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo}; + +const DATABASE_KEY_NAME: &str = "email_address"; +const EMAIL_REGEX: &str = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"; +const FEEDBACK_CHAR_LIMIT: RangeInclusive = 10..=5000; +const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = + "Feedback failed to submit, see error log for details."; + +#[derive(Serialize)] +struct FeedbackRequestBody<'a> { + feedback_text: &'a str, + email: Option, + metrics_id: Option>, + installation_id: Option>, + system_specs: SystemSpecs, + is_staff: bool, + token: &'a str, +} + +pub struct FeedbackModal { + system_specs: SystemSpecs, + feedback_editor: View, + email_address_editor: View, + character_count: usize, + pending_submission: bool, +} + +impl FocusableView for FeedbackModal { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.feedback_editor.focus_handle(cx) + } +} +impl EventEmitter for FeedbackModal {} + +impl FeedbackModal { + pub fn register(workspace: &mut Workspace, cx: &mut ViewContext) { + let _handle = cx.view().downgrade(); + workspace.register_action(move |workspace, _: &GiveFeedback, cx| { + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + + let project = workspace.project().clone(); + + cx.spawn(|workspace, mut cx| async move { + let markdown = markdown.await.log_err(); + let buffer = project + .update(&mut cx, |project, cx| { + project.create_buffer("", markdown, cx) + })? + .expect("creating buffers on a local workspace always succeeds"); + + workspace.update(&mut cx, |workspace, cx| { + let system_specs = SystemSpecs::new(cx); + + workspace.toggle_modal(cx, move |cx| { + FeedbackModal::new(system_specs, project, buffer, cx) + }); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + }); + } + + pub fn new( + system_specs: SystemSpecs, + project: Model, + buffer: Model, + cx: &mut ViewContext, + ) -> Self { + let email_address_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("Email address (optional)", cx); + + if let Ok(Some(email_address)) = KEY_VALUE_STORE.read_kvp(DATABASE_KEY_NAME) { + editor.set_text(email_address, cx) + } + + editor + }); + + let feedback_editor = cx.build_view(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx); + editor.set_vertical_scroll_margin(5, cx); + editor + }); + + cx.subscribe( + &feedback_editor, + |this, editor, event: &EditorEvent, cx| match event { + EditorEvent::Edited => { + this.character_count = editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("Feedback editor is never a multi-buffer") + .read(cx) + .len(); + cx.notify(); + } + _ => {} + }, + ) + .detach(); + + Self { + system_specs: system_specs.clone(), + feedback_editor, + email_address_editor, + pending_submission: false, + character_count: 0, + } + } + + pub fn submit(&mut self, cx: &mut ViewContext) -> Task> { + let feedback_text = self.feedback_editor.read(cx).text(cx).trim().to_string(); + let email = self.email_address_editor.read(cx).text_option(cx); + + let answer = cx.prompt( + PromptLevel::Info, + "Ready to submit your feedback?", + &["Yes, Submit!", "No"], + ); + let client = cx.global::>().clone(); + let specs = self.system_specs.clone(); + cx.spawn(|this, mut cx| async move { + let answer = answer.await.ok(); + if answer == Some(0) { + match email.clone() { + Some(email) => { + let _ = KEY_VALUE_STORE + .write_kvp(DATABASE_KEY_NAME.to_string(), email) + .await; + } + None => { + let _ = KEY_VALUE_STORE + .delete_kvp(DATABASE_KEY_NAME.to_string()) + .await; + } + }; + + this.update(&mut cx, |feedback_editor, cx| { + feedback_editor.set_pending_submission(true, cx); + }) + .log_err(); + + if let Err(error) = + FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await + { + log::error!("{}", error); + this.update(&mut cx, |feedback_editor, cx| { + let prompt = cx.prompt( + PromptLevel::Critical, + FEEDBACK_SUBMISSION_ERROR_TEXT, + &["OK"], + ); + cx.spawn(|_, _cx| async move { + prompt.await.ok(); + }) + .detach(); + feedback_editor.set_pending_submission(false, cx); + }) + .log_err(); + } + } + }) + .detach(); + Task::ready(Ok(())) + } + + fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext) { + self.pending_submission = pending_submission; + cx.notify(); + } + + async fn submit_feedback( + feedback_text: &str, + email: Option, + zed_client: Arc, + system_specs: SystemSpecs, + ) -> anyhow::Result<()> { + let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL); + let telemetry = zed_client.telemetry(); + let metrics_id = telemetry.metrics_id(); + let installation_id = telemetry.installation_id(); + let is_staff = telemetry.is_staff(); + let http_client = zed_client.http_client(); + let request = FeedbackRequestBody { + feedback_text: &feedback_text, + email, + metrics_id, + installation_id, + system_specs, + is_staff: is_staff.unwrap_or(false), + 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(); + if !response_status.is_success() { + bail!("Feedback API failed with error: {}", response_status) + } + Ok(()) + } + + // TODO: Escape button calls dismiss + // TODO: Should do same as hitting cancel / clicking outside of modal + // Close immediately if no text in field + // Ask to close if text in the field + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(DismissEvent); + } +} + +impl Render for FeedbackModal { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) { + Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address), + None => true, + }; + + let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count); + let characters_remaining = + if valid_character_count || self.character_count > *FEEDBACK_CHAR_LIMIT.end() { + *FEEDBACK_CHAR_LIMIT.end() as i32 - self.character_count as i32 + } else { + self.character_count as i32 - *FEEDBACK_CHAR_LIMIT.start() as i32 + }; + + let allow_submission = + valid_character_count && valid_email_address && !self.pending_submission; + + let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some(); + + let submit_button_text = if self.pending_submission { + "Sending..." + } else { + "Send Feedback" + }; + let dismiss = cx.listener(|_, _, cx| { + cx.emit(DismissEvent); + }); + // TODO: get the "are you sure you want to dismiss?" prompt here working + let dismiss_prompt = cx.listener(|_, _, _| { + // let answer = cx.prompt(PromptLevel::Info, "Exit feedback?", &["Yes", "No"]); + // cx.spawn(|_, _| async move { + // let answer = answer.await.ok(); + // if answer == Some(0) { + // cx.emit(DismissEvent); + // } + // }) + // .detach(); + }); + let open_community_repo = + cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); + + // TODO: Nate UI pass + v_stack() + .elevation_3(cx) + .key_context("GiveFeedback") + .on_action(cx.listener(Self::cancel)) + .min_w(rems(40.)) + .max_w(rems(96.)) + .border() + .border_color(red()) + .h(rems(40.)) + .p_2() + .gap_2() + .child( + v_stack().child( + div() + .size_full() + .child(Label::new("Give Feedback").color(Color::Default)) + .child(Label::new("This editor supports markdown").color(Color::Muted)), + ), + ) + .child( + div() + .flex_1() + .bg(cx.theme().colors().editor_background) + .border() + .border_color(cx.theme().colors().border) + .child(self.feedback_editor.clone()), + ) + .child( + div().child( + Label::new(format!( + "Characters: {}", + characters_remaining + )) + .when_else( + valid_character_count, + |this| this.color(Color::Success), + |this| this.color(Color::Error) + ) + ), + ) + .child( + div() + .bg(cx.theme().colors().editor_background) + .border() + .border_color(cx.theme().colors().border) + .child(self.email_address_editor.clone()) + ) + .child( + h_stack() + .justify_between() + .gap_1() + .child(Button::new("community_repo", "Community Repo") + .style(ButtonStyle::Filled) + .color(Color::Muted) + .on_click(open_community_repo) + ) + .child(h_stack().justify_between().gap_1() + .child( + Button::new("cancel_feedback", "Cancel") + .style(ButtonStyle::Subtle) + .color(Color::Muted) + // TODO: replicate this logic when clicking outside the modal + // TODO: Will require somehow overriding the modal dismal default behavior + .when_else( + has_feedback, + |this| this.on_click(dismiss_prompt), + |this| this.on_click(dismiss) + ) + ) + .child( + Button::new("send_feedback", submit_button_text) + .color(Color::Accent) + .style(ButtonStyle::Filled) + // TODO: Ensure that while submitting, "Sending..." is shown and disable the button + // TODO: If submit errors: show popup with error, don't close modal, set text back to "Send Feedback", and re-enable button + // TODO: If submit is successful, close the modal + .on_click(cx.listener(|this, _, cx| { + let _ = this.submit(cx); + })) + .tooltip(|cx| { + Tooltip::with_meta( + "Submit feedback to the Zed team.", + None, + "Provide an email address if you want us to be able to reply.", + cx, + ) + }) + .when(!allow_submission, |this| this.disabled(true)) + ), + ) + + ) + } +} diff --git a/crates/feedback2/src/system_specs.rs b/crates/feedback2/src/system_specs.rs new file mode 100644 index 0000000000..4d61867ba0 --- /dev/null +++ b/crates/feedback2/src/system_specs.rs @@ -0,0 +1,68 @@ +use client::ZED_APP_VERSION; +use gpui::AppContext; +use human_bytes::human_bytes; +use serde::Serialize; +use std::{env, fmt::Display}; +use sysinfo::{System, SystemExt}; +use util::channel::ReleaseChannel; + +#[derive(Clone, Debug, Serialize)] +pub struct SystemSpecs { + app_version: Option, + release_channel: &'static str, + os_name: &'static str, + os_version: Option, + memory: u64, + architecture: &'static str, +} + +impl SystemSpecs { + pub fn new(cx: &AppContext) -> Self { + let app_version = ZED_APP_VERSION + .or_else(|| cx.app_metadata().app_version) + .map(|v| v.to_string()); + let release_channel = cx.global::().dev_name(); + let os_name = cx.app_metadata().os_name; + let system = System::new_all(); + let memory = system.total_memory(); + let architecture = env::consts::ARCH; + let os_version = cx + .app_metadata() + .os_version + .map(|os_version| os_version.to_string()); + + SystemSpecs { + app_version, + release_channel, + os_name, + os_version, + memory, + architecture, + } + } +} + +impl Display for SystemSpecs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let os_information = match &self.os_version { + Some(os_version) => format!("OS: {} {}", self.os_name, os_version), + None => format!("OS: {}", self.os_name), + }; + let app_version_information = self + .app_version + .as_ref() + .map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel)); + let system_specs = [ + app_version_information, + Some(os_information), + Some(format!("Memory: {}", human_bytes(self.memory as f64))), + Some(format!("Architecture: {}", self.architecture)), + ] + .into_iter() + .flatten() + .collect::>() + .join("\n"); + + write!(f, "{system_specs}") + } +} diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 226a477012..a67276b0bc 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -69,6 +69,24 @@ pub trait IntoElement: Sized { self.map(|this| if condition { then(this) } else { this }) } + fn when_else( + self, + condition: bool, + then: impl FnOnce(Self) -> Self, + otherwise: impl FnOnce(Self) -> Self, + ) -> Self + where + Self: Sized, + { + self.map(|this| { + if condition { + then(this) + } else { + otherwise(this) + } + }) + } + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self where Self: Sized, diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 8a10173ca8..006640af4f 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -509,7 +509,7 @@ impl Default for CursorStyle { } } -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct SemanticVersion { major: usize, minor: usize, diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index c4054fa1a4..29586fd194 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -98,10 +98,11 @@ impl RenderOnce for Key { div() .py_0() - .when(single_char, |el| { - el.w(rems(14. / 16.)).flex().flex_none().justify_center() - }) - .when(!single_char, |el| el.px_0p5()) + .when_else( + single_char, + |el| el.w(rems(14. / 16.)).flex().flex_none().justify_center(), + |el| el.px_0p5(), + ) .h(rems(14. / 16.)) .text_ui() .line_height(relative(1.)) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index abf9089929..b660b8cb7f 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -68,6 +68,7 @@ use std::{ use theme::{ActiveTheme, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; +// use ui::{h_stack, v_stack, StyledExt}; use util::ResultExt; use uuid::Uuid; pub use workspace_settings::{AutosaveSetting, WorkspaceSettings}; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index e545fe3c97..ed7ed180f5 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -34,7 +34,7 @@ copilot_button = { package = "copilot_button2", path = "../copilot_button2" } diagnostics = { package = "diagnostics2", path = "../diagnostics2" } db = { package = "db2", path = "../db2" } editor = { package="editor2", path = "../editor2" } -# feedback = { path = "../feedback" } +feedback = { package="feedback2", path = "../feedback2" } file_finder = { package="file_finder2", path = "../file_finder2" } search = { package = "search2", path = "../search2" } fs = { package = "fs2", path = "../fs2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index f11c2eaadd..b97c4f40c3 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -221,7 +221,7 @@ fn main() { // language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); - // feedback::init(cx); + feedback::init(cx); welcome::init(cx); cx.set_menus(app_menus()); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 45ace75ebc..669b7816b3 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -102,6 +102,31 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.subscribe(&workspace_handle, { move |workspace, _, event, cx| { if let workspace::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace)); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); + toolbar.add_item(buffer_search_bar.clone(), cx); + // todo!() + // let quick_action_bar = cx.add_view(|_| { + // QuickActionBar::new(buffer_search_bar, workspace) + // }); + // toolbar.add_item(quick_action_bar, cx); + let diagnostic_editor_controls = + cx.build_view(|_| diagnostics::ToolbarControls::new()); + // toolbar.add_item(diagnostic_editor_controls, cx); + // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + // toolbar.add_item(project_search_bar, cx); + // let lsp_log_item = + // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); + // toolbar.add_item(lsp_log_item, cx); + // let syntax_tree_item = cx + // .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); + // toolbar.add_item(syntax_tree_item, cx); + }) + }); + initialize_pane(workspace, pane, cx); } } @@ -125,6 +150,9 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let active_buffer_language = cx.build_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); // let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx)); + let feedback_button = cx + .build_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)); + // let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); // let feedback_button = cx.add_view(|_| { // feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) // }); @@ -132,8 +160,8 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); - - // status_bar.add_right_item(feedback_button, cx); + status_bar.add_right_item(feedback_button, cx); + // status_bar.add_right_item(copilot, cx); status_bar.add_right_item(copilot, cx); status_bar.add_right_item(active_buffer_language, cx); // status_bar.add_right_item(vim_mode_indicator, cx);