Detailed changes
@@ -1784,7 +1784,7 @@ dependencies = [
"db2",
"editor2",
"feature_flags2",
- "feedback2",
+ "feedback",
"futures 0.3.28",
"fuzzy2",
"gpui2",
@@ -1793,7 +1793,7 @@ dependencies = [
"log",
"menu2",
"notifications2",
- "picker2",
+ "picker",
"postage",
"pretty_assertions",
"project2",
@@ -1859,7 +1859,7 @@ dependencies = [
"gpui2",
"language2",
"menu2",
- "picker2",
+ "picker",
"project2",
"serde",
"serde_json",
@@ -2976,36 +2976,6 @@ dependencies = [
[[package]]
name = "feedback"
version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "editor",
- "futures 0.3.28",
- "gpui",
- "human_bytes",
- "isahc",
- "language",
- "lazy_static",
- "log",
- "postage",
- "project",
- "regex",
- "search",
- "serde",
- "serde_derive",
- "settings",
- "smallvec",
- "sysinfo",
- "theme",
- "tree-sitter-markdown",
- "urlencoding",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "feedback2"
-version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 2.4.1",
@@ -3050,7 +3020,7 @@ dependencies = [
"gpui2",
"language2",
"menu2",
- "picker2",
+ "picker",
"postage",
"project2",
"serde",
@@ -4522,7 +4492,7 @@ dependencies = [
"fuzzy2",
"gpui2",
"language2",
- "picker2",
+ "picker",
"project2",
"settings2",
"theme2",
@@ -5850,7 +5820,7 @@ dependencies = [
"gpui2",
"language2",
"ordered-float 2.10.0",
- "picker2",
+ "picker",
"postage",
"settings2",
"smol",
@@ -6067,23 +6037,6 @@ dependencies = [
[[package]]
name = "picker"
version = "0.1.0"
-dependencies = [
- "ctor",
- "editor",
- "env_logger",
- "gpui",
- "menu",
- "parking_lot 0.11.2",
- "serde_json",
- "settings",
- "theme",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "picker2"
-version = "0.1.0"
dependencies = [
"ctor",
"editor2",
@@ -6540,7 +6493,7 @@ dependencies = [
"language2",
"lsp2",
"ordered-float 2.10.0",
- "picker2",
+ "picker",
"postage",
"project2",
"settings2",
@@ -6890,7 +6843,7 @@ dependencies = [
"gpui2",
"language2",
"ordered-float 2.10.0",
- "picker2",
+ "picker",
"postage",
"settings2",
"smol",
@@ -8660,7 +8613,7 @@ dependencies = [
"language2",
"log",
"menu2",
- "picker2",
+ "picker",
"rust-embed",
"serde",
"settings2",
@@ -9168,7 +9121,7 @@ dependencies = [
"gpui2",
"log",
"parking_lot 0.11.2",
- "picker2",
+ "picker",
"postage",
"settings2",
"smol",
@@ -10236,7 +10189,7 @@ dependencies = [
"fs2",
"fuzzy2",
"gpui2",
- "picker2",
+ "picker",
"ui2",
"util",
"workspace2",
@@ -10682,7 +10635,7 @@ dependencies = [
"gpui2",
"install_cli2",
"log",
- "picker2",
+ "picker",
"project2",
"schemars",
"serde",
@@ -11122,7 +11075,7 @@ dependencies = [
"editor2",
"env_logger",
"feature_flags2",
- "feedback2",
+ "feedback",
"file_finder",
"fs2",
"fsevent",
@@ -71,7 +71,6 @@ members = [
"crates/outline",
"crates/outline2",
"crates/picker",
- "crates/picker2",
"crates/plugin",
"crates/plugin_macros",
"crates/prettier",
@@ -32,14 +32,14 @@ collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
# drag_and_drop = { path = "../drag_and_drop" }
editor = { package="editor2", path = "../editor2" }
-feedback = { package = "feedback2", path = "../feedback2" }
+feedback = { path = "../feedback" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
notifications = { package = "notifications2", path = "../notifications2" }
rich_text = { package = "rich_text2", path = "../rich_text2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
project = { package = "project2", path = "../project2" }
recent_projects = { path = "../recent_projects" }
rpc = { package ="rpc2", path = "../rpc2" }
@@ -13,7 +13,7 @@ collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
ui = { package = "ui2", path = "../ui2" }
@@ -0,0 +1,36 @@
+[package]
+name = "command_palette"
+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 = { path = "../picker" }
+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
@@ -11,31 +11,37 @@ path = "src/feedback.rs"
test-support = []
[dependencies]
-client = { path = "../client" }
-editor = { path = "../editor" }
-language = { path = "../language" }
-gpui = { path = "../gpui" }
-project = { path = "../project" }
-regex.workspace = true
+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" }
search = { path = "../search" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
-workspace = { path = "../workspace" }
+workspace = { package = "workspace2", path = "../workspace2"}
-log.workspace = true
-futures.workspace = true
-anyhow.workspace = true
-smallvec.workspace = true
+bitflags = "2.4.1"
human_bytes = "0.4.1"
+
+anyhow.workspace = true
+futures.workspace = true
isahc.workspace = true
lazy_static.workspace = true
+log.workspace = true
postage.workspace = true
+regex.workspace = true
serde.workspace = true
serde_derive.workspace = true
+smallvec.workspace = true
+smol.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 = { path = "../editor", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
@@ -1,91 +1,49 @@
-use gpui::{
- elements::*,
- platform::{CursorStyle, MouseButton},
- Entity, View, ViewContext, WeakViewHandle,
-};
+use gpui::{Render, ViewContext, WeakView};
+use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
-use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
+use crate::{feedback_modal::FeedbackModal, GiveFeedback};
pub struct DeployFeedbackButton {
- active: bool,
- workspace: WeakViewHandle<Workspace>,
-}
-
-impl Entity for DeployFeedbackButton {
- type Event = ();
+ workspace: WeakView<Workspace>,
}
impl DeployFeedbackButton {
pub fn new(workspace: &Workspace) -> Self {
DeployFeedbackButton {
- active: false,
workspace: workspace.weak_handle(),
}
}
}
-impl View for DeployFeedbackButton {
- fn ui_name() -> &'static str {
- "DeployFeedbackButton"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let active = self.active;
- let theme = theme::current(cx).clone();
- Stack::new()
- .with_child(
- MouseEventHandler::new::<Self, _>(0, cx, |state, _| {
- let style = &theme
- .workspace
- .status_bar
- .panel_buttons
- .button
- .in_state(active)
- .style_for(state);
-
- Svg::new("icons/feedback.svg")
- .with_color(style.icon_color)
- .constrained()
- .with_width(style.icon_size)
- .aligned()
- .constrained()
- .with_width(style.icon_size)
- .with_height(style.icon_size)
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if !active {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace
- .update(cx, |workspace, cx| FeedbackEditor::deploy(workspace, cx))
- }
- }
+impl Render for DeployFeedbackButton {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let is_open = self
+ .workspace
+ .upgrade()
+ .and_then(|workspace| {
+ workspace.update(cx, |workspace, cx| {
+ workspace.active_modal::<FeedbackModal>(cx)
})
- .with_tooltip::<Self>(
- 0,
- "Send Feedback",
- Some(Box::new(GiveFeedback)),
- theme.tooltip.clone(),
- cx,
- ),
- )
- .into_any()
+ })
+ .is_some();
+ IconButton::new("give-feedback", Icon::Envelope)
+ .style(ui::ButtonStyle::Subtle)
+ .icon_size(IconSize::Small)
+ .selected(is_open)
+ .tooltip(|cx| Tooltip::text("Share 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<Self>) {
- if let Some(item) = item {
- if let Some(_) = item.downcast::<FeedbackEditor>() {
- self.active = true;
- cx.notify();
- return;
- }
- }
- self.active = false;
- cx.notify();
+ fn set_active_pane_item(
+ &mut self,
+ _item: Option<&dyn ItemHandle>,
+ _cx: &mut ViewContext<Self>,
+ ) {
}
}
@@ -1,12 +1,13 @@
+use gpui::{actions, AppContext, ClipboardItem, PromptLevel};
+use system_specs::SystemSpecs;
+use workspace::Workspace;
+
pub mod deploy_feedback_button;
-pub mod feedback_editor;
-pub mod feedback_info_text;
-pub mod submit_feedback_button;
+pub mod feedback_modal;
+
+actions!(feedback, [GiveFeedback, SubmitFeedback]);
mod system_specs;
-use gpui::{actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext};
-use system_specs::SystemSpecs;
-use workspace::Workspace;
actions!(
zed,
@@ -19,44 +20,42 @@ actions!(
);
pub fn init(cx: &mut AppContext) {
- feedback_editor::init(cx);
-
- cx.add_action(
- move |_: &mut Workspace,
- _: &CopySystemSpecsIntoClipboard,
- cx: &mut ViewContext<Workspace>| {
- let specs = SystemSpecs::new(&cx).to_string();
- cx.prompt(
- PromptLevel::Info,
- &format!("Copied into clipboard:\n\n{specs}"),
- &["OK"],
- );
- let item = ClipboardItem::new(specs.clone());
- cx.write_to_clipboard(item);
- },
- );
-
- cx.add_action(
- |_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
- let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
- cx.platform().open_url(url);
- },
- );
-
- cx.add_action(
- move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
- 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.platform().open_url(&url);
- },
- );
-
- cx.add_global_action(open_zed_community_repo);
-}
-
-pub fn open_zed_community_repo(_: &OpenZedCommunityRepo, cx: &mut AppContext) {
- let url = "https://github.com/zed-industries/community";
- cx.platform().open_url(&url);
+ // 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();
}
@@ -1,442 +0,0 @@
-use crate::system_specs::SystemSpecs;
-use anyhow::bail;
-use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
-use editor::{Anchor, Editor};
-use futures::AsyncReadExt;
-use gpui::{
- actions,
- elements::{ChildView, Flex, Label, ParentElement, Svg},
- platform::PromptLevel,
- serde_json, AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View,
- ViewContext, ViewHandle,
-};
-use isahc::Request;
-use language::Buffer;
-use postage::prelude::Stream;
-use project::{search::SearchQuery, Project};
-use regex::Regex;
-use serde::Serialize;
-use smallvec::SmallVec;
-use std::{
- any::TypeId,
- borrow::Cow,
- ops::{Range, RangeInclusive},
- sync::Arc,
-};
-use util::ResultExt;
-use workspace::{
- item::{Item, ItemEvent, ItemHandle},
- searchable::{SearchableItem, SearchableItemHandle},
- Workspace,
-};
-
-const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
-const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
- "Feedback failed to submit, see error log for details.";
-
-actions!(feedback, [GiveFeedback, SubmitFeedback]);
-
-pub fn init(cx: &mut AppContext) {
- cx.add_action({
- move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
- FeedbackEditor::deploy(workspace, cx);
- }
- });
-}
-
-#[derive(Serialize)]
-struct FeedbackRequestBody<'a> {
- feedback_text: &'a str,
- email: Option<String>,
- metrics_id: Option<Arc<str>>,
- installation_id: Option<Arc<str>>,
- system_specs: SystemSpecs,
- is_staff: bool,
- token: &'a str,
-}
-
-#[derive(Clone)]
-pub(crate) struct FeedbackEditor {
- system_specs: SystemSpecs,
- editor: ViewHandle<Editor>,
- project: ModelHandle<Project>,
- pub allow_submission: bool,
-}
-
-impl FeedbackEditor {
- fn new(
- system_specs: SystemSpecs,
- project: ModelHandle<Project>,
- buffer: ModelHandle<Buffer>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let editor = cx.add_view(|cx| {
- let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
- editor.set_vertical_scroll_margin(5, cx);
- editor
- });
-
- cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()))
- .detach();
-
- Self {
- system_specs: system_specs.clone(),
- editor,
- project,
- allow_submission: true,
- }
- }
-
- pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
- if !self.allow_submission {
- return Task::ready(Ok(()));
- }
-
- let feedback_text = self.editor.read(cx).text(cx);
- let feedback_char_count = feedback_text.chars().count();
- let feedback_text = feedback_text.trim().to_string();
-
- let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
- Some(format!(
- "Feedback can't be shorter than {} characters.",
- FEEDBACK_CHAR_LIMIT.start()
- ))
- } else if feedback_char_count > *FEEDBACK_CHAR_LIMIT.end() {
- Some(format!(
- "Feedback can't be longer than {} characters.",
- FEEDBACK_CHAR_LIMIT.end()
- ))
- } else {
- None
- };
-
- if let Some(error) = error {
- cx.prompt(PromptLevel::Critical, &error, &["OK"]);
- return Task::ready(Ok(()));
- }
-
- let mut answer = cx.prompt(
- PromptLevel::Info,
- "Ready to submit your feedback?",
- &["Yes, Submit!", "No"],
- );
-
- let client = cx.global::<Arc<Client>>().clone();
- let specs = self.system_specs.clone();
-
- cx.spawn(|this, mut cx| async move {
- let answer = answer.recv().await;
-
- if answer == Some(0) {
- this.update(&mut cx, |feedback_editor, cx| {
- feedback_editor.set_allow_submission(false, cx);
- })
- .log_err();
-
- match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
- Ok(_) => {
- this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed))
- .log_err();
- }
-
- Err(error) => {
- log::error!("{}", error);
- this.update(&mut cx, |feedback_editor, cx| {
- cx.prompt(
- PromptLevel::Critical,
- FEEDBACK_SUBMISSION_ERROR_TEXT,
- &["OK"],
- );
- feedback_editor.set_allow_submission(true, cx);
- })
- .log_err();
- }
- }
- }
- })
- .detach();
-
- Task::ready(Ok(()))
- }
-
- fn set_allow_submission(&mut self, allow_submission: bool, cx: &mut ViewContext<Self>) {
- self.allow_submission = allow_submission;
- cx.notify();
- }
-
- async fn submit_feedback(
- feedback_text: &str,
- zed_client: Arc<Client>,
- 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 re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
-
- let emails: Vec<&str> = re
- .captures_iter(feedback_text)
- .map(|capture| capture.get(0).unwrap().as_str())
- .collect();
-
- let email = emails.first().map(|e| e.to_string());
-
- 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(())
- }
-}
-
-impl FeedbackEditor {
- pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
- let markdown = workspace
- .app_state()
- .languages
- .language_for_name("Markdown");
- cx.spawn(|workspace, mut cx| async move {
- let markdown = markdown.await.log_err();
- workspace
- .update(&mut cx, |workspace, cx| {
- workspace.with_local_workspace(cx, |workspace, cx| {
- let project = workspace.project().clone();
- let buffer = project
- .update(cx, |project, cx| project.create_buffer("", markdown, cx))
- .expect("creating buffers on a local workspace always succeeds");
- let system_specs = SystemSpecs::new(cx);
- let feedback_editor = cx
- .add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
- workspace.add_item(Box::new(feedback_editor), cx);
- })
- })?
- .await
- })
- .detach_and_log_err(cx);
- }
-}
-
-impl View for FeedbackEditor {
- fn ui_name() -> &'static str {
- "FeedbackEditor"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- ChildView::new(&self.editor, cx).into_any()
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- if cx.is_self_focused() {
- cx.focus(&self.editor);
- }
- }
-}
-
-impl Entity for FeedbackEditor {
- type Event = editor::Event;
-}
-
-impl Item for FeedbackEditor {
- fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
- Some("Send Feedback".into())
- }
-
- fn tab_content<T: 'static>(
- &self,
- _: Option<usize>,
- style: &theme::Tab,
- _: &AppContext,
- ) -> AnyElement<T> {
- Flex::row()
- .with_child(
- Svg::new("icons/feedback.svg")
- .with_color(style.label.text.color)
- .constrained()
- .with_width(style.type_icon_width)
- .aligned()
- .contained()
- .with_margin_right(style.spacing),
- )
- .with_child(
- Label::new("Send Feedback", style.label.clone())
- .aligned()
- .contained(),
- )
- .into_any()
- }
-
- 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 {
- true
- }
-
- fn can_save(&self, _: &AppContext) -> bool {
- true
- }
-
- fn save(
- &mut self,
- _: ModelHandle<Project>,
- cx: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>> {
- self.submit(cx)
- }
-
- fn save_as(
- &mut self,
- _: ModelHandle<Project>,
- _: std::path::PathBuf,
- cx: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>> {
- self.submit(cx)
- }
-
- fn reload(
- &mut self,
- _: ModelHandle<Project>,
- _: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>> {
- Task::Ready(Some(Ok(())))
- }
-
- fn clone_on_split(
- &self,
- _workspace_id: workspace::WorkspaceId,
- cx: &mut ViewContext<Self>,
- ) -> Option<Self>
- where
- Self: Sized,
- {
- let buffer = self
- .editor
- .read(cx)
- .buffer()
- .read(cx)
- .as_singleton()
- .expect("Feedback buffer is only ever singleton");
-
- Some(Self::new(
- self.system_specs.clone(),
- self.project.clone(),
- buffer.clone(),
- cx,
- ))
- }
-
- fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(handle.clone()))
- }
-
- fn act_as_type<'a>(
- &'a self,
- type_id: TypeId,
- self_handle: &'a ViewHandle<Self>,
- _: &'a AppContext,
- ) -> Option<&'a AnyViewHandle> {
- if type_id == TypeId::of::<Self>() {
- Some(self_handle)
- } else if type_id == TypeId::of::<Editor>() {
- Some(&self.editor)
- } else {
- None
- }
- }
-
- fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
- Editor::to_item_events(event)
- }
-}
-
-impl SearchableItem for FeedbackEditor {
- type Match = Range<Anchor>;
-
- fn to_search_event(
- &mut self,
- event: &Self::Event,
- cx: &mut ViewContext<Self>,
- ) -> Option<workspace::searchable::SearchEvent> {
- self.editor
- .update(cx, |editor, cx| editor.to_search_event(event, cx))
- }
-
- fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| editor.clear_matches(cx))
- }
-
- fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| editor.update_matches(matches, cx))
- }
-
- fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
- self.editor
- .update(cx, |editor, cx| editor.query_suggestion(cx))
- }
-
- fn activate_match(
- &mut self,
- index: usize,
- matches: Vec<Self::Match>,
- cx: &mut ViewContext<Self>,
- ) {
- self.editor
- .update(cx, |editor, cx| editor.activate_match(index, matches, cx))
- }
-
- fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |e, cx| e.select_matches(matches, cx))
- }
- fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |e, cx| e.replace(matches, query, cx));
- }
- fn find_matches(
- &mut self,
- query: Arc<project::search::SearchQuery>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Vec<Self::Match>> {
- self.editor
- .update(cx, |editor, cx| editor.find_matches(query, cx))
- }
-
- fn active_match_index(
- &mut self,
- matches: Vec<Self::Match>,
- cx: &mut ViewContext<Self>,
- ) -> Option<usize> {
- self.editor
- .update(cx, |editor, cx| editor.active_match_index(matches, cx))
- }
-}
@@ -1,94 +0,0 @@
-use gpui::{
- elements::{Flex, Label, MouseEventHandler, ParentElement, Text},
- platform::{CursorStyle, MouseButton},
- AnyElement, Element, Entity, View, ViewContext, ViewHandle,
-};
-use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
-
-use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo};
-
-pub struct FeedbackInfoText {
- active_item: Option<ViewHandle<FeedbackEditor>>,
-}
-
-impl FeedbackInfoText {
- pub fn new() -> Self {
- Self {
- active_item: Default::default(),
- }
- }
-}
-
-impl Entity for FeedbackInfoText {
- type Event = ();
-}
-
-impl View for FeedbackInfoText {
- fn ui_name() -> &'static str {
- "FeedbackInfoText"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = theme::current(cx).clone();
-
- Flex::row()
- .with_child(
- Text::new(
- "Share your feedback. Include your email for replies. For issues and discussions, visit the ",
- theme.feedback.info_text_default.text.clone(),
- )
- .with_soft_wrap(false)
- .aligned(),
- )
- .with_child(
- MouseEventHandler::new::<OpenZedCommunityRepo, _>(0, cx, |state, _| {
- let style = if state.hovered() {
- &theme.feedback.link_text_hover
- } else {
- &theme.feedback.link_text_default
- };
- Label::new("community repo", style.text.clone())
- .contained()
- .with_style(style.container)
- .aligned()
- .left()
- .clipped()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, _, cx| {
- open_zed_community_repo(&Default::default(), cx)
- }),
- )
- .with_child(
- Text::new(".", theme.feedback.info_text_default.text.clone())
- .with_soft_wrap(false)
- .aligned(),
- )
- .contained()
- .with_style(theme.feedback.info_text_default.container)
- .aligned()
- .left()
- .clipped()
- .into_any()
- }
-}
-
-impl ToolbarItemView for FeedbackInfoText {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) -> workspace::ToolbarItemLocation {
- cx.notify();
- if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
- {
- self.active_item = Some(feedback_editor);
- ToolbarItemLocation::PrimaryLeft {
- flex: Some((1., false)),
- }
- } else {
- self.active_item = None;
- ToolbarItemLocation::Hidden
- }
- }
-}
@@ -1,108 +0,0 @@
-use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
-use anyhow::Result;
-use gpui::{
- elements::{Label, MouseEventHandler},
- platform::{CursorStyle, MouseButton},
- AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
-};
-use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
-
-pub fn init(cx: &mut AppContext) {
- cx.add_async_action(SubmitFeedbackButton::submit);
-}
-
-pub struct SubmitFeedbackButton {
- pub(crate) active_item: Option<ViewHandle<FeedbackEditor>>,
-}
-
-impl SubmitFeedbackButton {
- pub fn new() -> Self {
- Self {
- active_item: Default::default(),
- }
- }
-
- pub fn submit(
- &mut self,
- _: &SubmitFeedback,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- if let Some(active_item) = self.active_item.as_ref() {
- Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.submit(cx)))
- } else {
- None
- }
- }
-}
-
-impl Entity for SubmitFeedbackButton {
- type Event = ();
-}
-
-impl View for SubmitFeedbackButton {
- fn ui_name() -> &'static str {
- "SubmitFeedbackButton"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = theme::current(cx).clone();
- let allow_submission = self
- .active_item
- .as_ref()
- .map_or(true, |i| i.read(cx).allow_submission);
-
- enum SubmitFeedbackButton {}
- MouseEventHandler::new::<SubmitFeedbackButton, _>(0, cx, |state, _| {
- let text;
- let style = if allow_submission {
- text = "Submit as Markdown";
- theme.feedback.submit_button.style_for(state)
- } else {
- text = "Submitting...";
- theme
- .feedback
- .submit_button
- .disabled
- .as_ref()
- .unwrap_or(&theme.feedback.submit_button.default)
- };
-
- Label::new(text, style.text.clone())
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, this, cx| {
- this.submit(&Default::default(), cx);
- })
- .aligned()
- .contained()
- .with_margin_left(theme.feedback.button_margin)
- .with_tooltip::<Self>(
- 0,
- "cmd-s",
- Some(Box::new(SubmitFeedback)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
- }
-}
-
-impl ToolbarItemView for SubmitFeedbackButton {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) -> workspace::ToolbarItemLocation {
- cx.notify();
- if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
- {
- self.active_item = Some(feedback_editor);
- ToolbarItemLocation::PrimaryRight { flex: None }
- } else {
- self.active_item = None;
- ToolbarItemLocation::Hidden
- }
- }
-}
@@ -1,17 +1,14 @@
use client::ZED_APP_VERSION;
-use gpui::{platform::AppVersion, AppContext};
+use gpui::AppContext;
use human_bytes::human_bytes;
use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{System, SystemExt};
use util::channel::ReleaseChannel;
-// TODO: Move this file out of feedback and into a more general place
-
#[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs {
- #[serde(serialize_with = "serialize_app_version")]
- app_version: Option<AppVersion>,
+ app_version: Option<String>,
release_channel: &'static str,
os_name: &'static str,
os_version: Option<String>,
@@ -21,16 +18,17 @@ pub struct SystemSpecs {
impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self {
- let platform = cx.platform();
- let app_version = ZED_APP_VERSION.or_else(|| platform.app_version().ok());
+ let app_version = ZED_APP_VERSION
+ .or_else(|| cx.app_metadata().app_version)
+ .map(|v| v.to_string());
let release_channel = cx.global::<ReleaseChannel>().display_name();
- let os_name = platform.os_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 = platform
- .os_version()
- .ok()
+ let os_version = cx
+ .app_metadata()
+ .os_version
.map(|os_version| os_version.to_string());
SystemSpecs {
@@ -68,10 +66,3 @@ impl Display for SystemSpecs {
write!(f, "{system_specs}")
}
}
-
-fn serialize_app_version<S>(version: &Option<AppVersion>, serializer: S) -> Result<S::Ok, S::Error>
-where
- S: serde::Serializer,
-{
- version.map(|v| v.to_string()).serialize(serializer)
-}
@@ -1,47 +0,0 @@
-[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" }
-search = { path = "../search" }
-settings = { package = "settings2", path = "../settings2" }
-theme = { package = "theme2", path = "../theme2" }
-ui = { package = "ui2", path = "../ui2" }
-util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2"}
-
-bitflags = "2.4.1"
-human_bytes = "0.4.1"
-
-anyhow.workspace = true
-futures.workspace = true
-isahc.workspace = true
-lazy_static.workspace = true
-log.workspace = true
-postage.workspace = true
-regex.workspace = true
-serde.workspace = true
-serde_derive.workspace = true
-smallvec.workspace = true
-smol.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"] }
@@ -1,49 +0,0 @@
-use gpui::{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<Workspace>,
-}
-
-impl DeployFeedbackButton {
- pub fn new(workspace: &Workspace) -> Self {
- DeployFeedbackButton {
- workspace: workspace.weak_handle(),
- }
- }
-}
-
-impl Render for DeployFeedbackButton {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let is_open = self
- .workspace
- .upgrade()
- .and_then(|workspace| {
- workspace.update(cx, |workspace, cx| {
- workspace.active_modal::<FeedbackModal>(cx)
- })
- })
- .is_some();
- IconButton::new("give-feedback", Icon::Envelope)
- .style(ui::ButtonStyle::Subtle)
- .icon_size(IconSize::Small)
- .selected(is_open)
- .tooltip(|cx| Tooltip::text("Share 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<Self>,
- ) {
- }
-}
@@ -1,61 +0,0 @@
-use gpui::{actions, AppContext, ClipboardItem, PromptLevel};
-use system_specs::SystemSpecs;
-use workspace::Workspace;
-
-pub mod deploy_feedback_button;
-pub mod feedback_modal;
-
-actions!(feedback, [GiveFeedback, SubmitFeedback]);
-
-mod system_specs;
-
-actions!(
- zed,
- [
- 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();
-}
@@ -1,68 +0,0 @@
-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<String>,
- release_channel: &'static str,
- os_name: &'static str,
- os_version: Option<String>,
- 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::<ReleaseChannel>().display_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::<Vec<String>>()
- .join("\n");
-
- write!(f, "{system_specs}")
- }
-}
@@ -14,7 +14,7 @@ collections = { path = "../collections" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
menu = { package = "menu2", path = "../menu2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
text = { package = "text2", path = "../text2" }
@@ -13,7 +13,7 @@ editor = { package = "editor2", path = "../editor2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
project = { package = "project2", path = "../project2" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
@@ -14,7 +14,7 @@ fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" }
language = { package = "language2", path = "../language2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
settings = { package = "settings2", path = "../settings2" }
text = { package = "text2", path = "../text2" }
theme = { package = "theme2", path = "../theme2" }
@@ -9,20 +9,20 @@ path = "src/picker.rs"
doctest = false
[dependencies]
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-menu = { path = "../menu" }
-settings = { path = "../settings" }
+editor = { package = "editor2", path = "../editor2" }
+ui = { package = "ui2", path = "../ui2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+menu = { package = "menu2", path = "../menu2" }
+settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2"}
parking_lot.workspace = true
[dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
serde_json.workspace = true
-workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
@@ -1,267 +1,204 @@
use editor::Editor;
use gpui::{
- elements::*,
- geometry::vector::{vec2f, Vector2F},
- keymap_matcher::KeymapContext,
- platform::{CursorStyle, MouseButton},
- AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext,
- ViewHandle,
+ div, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
+ FocusableView, Length, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle,
+ View, ViewContext, WindowContext,
};
-use menu::{Cancel, Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
-use parking_lot::Mutex;
use std::{cmp, sync::Arc};
-use util::ResultExt;
-use workspace::Modal;
-
-#[derive(Clone, Copy)]
-pub enum PickerEvent {
- Dismiss,
-}
+use ui::{prelude::*, v_stack, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator};
+use workspace::ModalView;
pub struct Picker<D: PickerDelegate> {
- delegate: D,
- query_editor: ViewHandle<Editor>,
- list_state: UniformListState,
- max_size: Vector2F,
- theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
- confirmed: bool,
- pending_update_matches: Option<Task<Option<()>>>,
+ pub delegate: D,
+ scroll_handle: UniformListScrollHandle,
+ editor: View<Editor>,
+ pending_update_matches: Option<Task<()>>,
confirm_on_update: Option<bool>,
- has_focus: bool,
+ width: Option<Length>,
+ max_height: Option<Length>,
+
+ /// Whether the `Picker` is rendered as a self-contained modal.
+ ///
+ /// Set this to `false` when rendering the `Picker` as part of a larger modal.
+ is_modal: bool,
}
pub trait PickerDelegate: Sized + 'static {
- fn placeholder_text(&self) -> Arc<str>;
+ type ListItem: IntoElement;
fn match_count(&self) -> usize;
fn selected_index(&self) -> usize;
+ fn separators_after_indices(&self) -> Vec<usize> {
+ Vec::new()
+ }
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
+
+ fn placeholder_text(&self) -> Arc<str>;
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
+
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
+
fn render_match(
&self,
ix: usize,
- state: &mut MouseState,
selected: bool,
- cx: &AppContext,
- ) -> AnyElement<Picker<Self>>;
- fn center_selection_after_match_updates(&self) -> bool {
- false
- }
- fn render_header(
- &self,
- _cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<AnyElement<Picker<Self>>> {
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem>;
+ fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
None
}
- fn render_footer(
- &self,
- _cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<AnyElement<Picker<Self>>> {
+ fn render_footer(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
None
}
}
-impl<D: PickerDelegate> Entity for Picker<D> {
- type Event = PickerEvent;
-}
-
-impl<D: PickerDelegate> View for Picker<D> {
- fn ui_name() -> &'static str {
- "Picker"
+impl<D: PickerDelegate> FocusableView for Picker<D> {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.editor.focus_handle(cx)
}
+}
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = (self.theme.lock())(theme::current(cx).as_ref());
- let query = self.query(cx);
- let match_count = self.delegate.match_count();
-
- let container_style;
- let editor_style;
- if query.is_empty() && match_count == 0 {
- container_style = theme.empty_container;
- editor_style = theme.empty_input_editor.container;
- } else {
- container_style = theme.container;
- editor_style = theme.input_editor.container;
+impl<D: PickerDelegate> Picker<D> {
+ pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
+ let editor = cx.new_view(|cx| {
+ let mut editor = Editor::single_line(cx);
+ editor.set_placeholder_text(delegate.placeholder_text(), cx);
+ editor
+ });
+ cx.subscribe(&editor, Self::on_input_editor_event).detach();
+ let mut this = Self {
+ delegate,
+ editor,
+ scroll_handle: UniformListScrollHandle::new(),
+ pending_update_matches: None,
+ confirm_on_update: None,
+ width: None,
+ max_height: None,
+ is_modal: true,
};
-
- Flex::new(Axis::Vertical)
- .with_child(
- ChildView::new(&self.query_editor, cx)
- .contained()
- .with_style(editor_style),
- )
- .with_children(self.delegate.render_header(cx))
- .with_children(if match_count == 0 {
- if query.is_empty() {
- None
- } else {
- Some(
- Label::new("No matches", theme.no_matches.label.clone())
- .contained()
- .with_style(theme.no_matches.container)
- .into_any(),
- )
- }
- } else {
- Some(
- UniformList::new(
- self.list_state.clone(),
- match_count,
- cx,
- move |this, mut range, items, cx| {
- let selected_ix = this.delegate.selected_index();
- range.end = cmp::min(range.end, this.delegate.match_count());
- items.extend(range.map(move |ix| {
- MouseEventHandler::new::<D, _>(ix, cx, |state, cx| {
- this.delegate.render_match(ix, state, ix == selected_ix, cx)
- })
- // Capture mouse events
- .on_down(MouseButton::Left, |_, _, _| {})
- .on_up(MouseButton::Left, |_, _, _| {})
- .on_click(MouseButton::Left, move |click, picker, cx| {
- picker.select_index(ix, click.cmd, cx);
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .into_any()
- }));
- },
- )
- .contained()
- .with_margin_top(6.0)
- .flex(1., false)
- .into_any(),
- )
- })
- .with_children(self.delegate.render_footer(cx))
- .contained()
- .with_style(container_style)
- .constrained()
- .with_max_width(self.max_size.x())
- .with_max_height(self.max_size.y())
- .into_any_named("picker")
+ this.update_matches("".to_string(), cx);
+ this
}
- fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
- Self::reset_to_default_keymap_context(keymap);
- keymap.add_identifier("menu");
+ pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
+ self.width = Some(width.into());
+ self
}
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
- if cx.is_self_focused() {
- cx.focus(&self.query_editor);
- }
+ pub fn max_height(mut self, max_height: impl Into<gpui::Length>) -> Self {
+ self.max_height = Some(max_height.into());
+ self
}
- fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
+ pub fn modal(mut self, modal: bool) -> Self {
+ self.is_modal = modal;
+ self
}
-}
-impl<D: PickerDelegate> Modal for Picker<D> {
- fn has_focus(&self) -> bool {
- self.has_focus
+ pub fn focus(&self, cx: &mut WindowContext) {
+ self.editor.update(cx, |editor, cx| editor.focus(cx));
}
- fn dismiss_on_event(event: &Self::Event) -> bool {
- matches!(event, PickerEvent::Dismiss)
+ pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
+ let count = self.delegate.match_count();
+ if count > 0 {
+ let index = self.delegate.selected_index();
+ let ix = cmp::min(index + 1, count - 1);
+ self.delegate.set_selected_index(ix, cx);
+ self.scroll_handle.scroll_to_item(ix);
+ cx.notify();
+ }
}
-}
-impl<D: PickerDelegate> Picker<D> {
- pub fn init(cx: &mut AppContext) {
- cx.add_action(Self::select_first);
- cx.add_action(Self::select_last);
- cx.add_action(Self::select_next);
- cx.add_action(Self::select_prev);
- cx.add_action(Self::confirm);
- cx.add_action(Self::secondary_confirm);
- cx.add_action(Self::cancel);
+ fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
+ let count = self.delegate.match_count();
+ if count > 0 {
+ let index = self.delegate.selected_index();
+ let ix = index.saturating_sub(1);
+ self.delegate.set_selected_index(ix, cx);
+ self.scroll_handle.scroll_to_item(ix);
+ cx.notify();
+ }
}
- pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
- let theme = Arc::new(Mutex::new(
- Box::new(|theme: &theme::Theme| theme.picker.clone())
- as Box<dyn Fn(&theme::Theme) -> theme::Picker>,
- ));
- let placeholder_text = delegate.placeholder_text();
- let query_editor = cx.add_view({
- let picker_theme = theme.clone();
- |cx| {
- let mut editor = Editor::single_line(
- Some(Arc::new(move |theme| {
- (picker_theme.lock())(theme).input_editor.clone()
- })),
- cx,
- );
- editor.set_placeholder_text(placeholder_text, cx);
- editor
- }
- });
- cx.subscribe(&query_editor, Self::on_query_editor_event)
- .detach();
- let mut this = Self {
- query_editor,
- list_state: Default::default(),
- delegate,
- max_size: vec2f(540., 420.),
- theme,
- confirmed: false,
- pending_update_matches: None,
- confirm_on_update: None,
- has_focus: false,
- };
- this.update_matches(String::new(), cx);
- this
+ fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
+ let count = self.delegate.match_count();
+ if count > 0 {
+ self.delegate.set_selected_index(0, cx);
+ self.scroll_handle.scroll_to_item(0);
+ cx.notify();
+ }
}
- pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
- self.max_size = vec2f(width, height);
- self
+ fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
+ let count = self.delegate.match_count();
+ if count > 0 {
+ self.delegate.set_selected_index(count - 1, cx);
+ self.scroll_handle.scroll_to_item(count - 1);
+ cx.notify();
+ }
}
- pub fn with_theme<F>(self, theme: F) -> Self
- where
- F: 'static + Fn(&theme::Theme) -> theme::Picker,
- {
- *self.theme.lock() = Box::new(theme);
- self
+ pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
+ let count = self.delegate.match_count();
+ let index = self.delegate.selected_index();
+ let new_index = if index + 1 == count { 0 } else { index + 1 };
+ self.delegate.set_selected_index(new_index, cx);
+ self.scroll_handle.scroll_to_item(new_index);
+ cx.notify();
}
- pub fn delegate(&self) -> &D {
- &self.delegate
+ pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+ self.delegate.dismissed(cx);
+ cx.emit(DismissEvent);
}
- pub fn delegate_mut(&mut self) -> &mut D {
- &mut self.delegate
+ fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+ if self.pending_update_matches.is_some() {
+ self.confirm_on_update = Some(false)
+ } else {
+ self.delegate.confirm(false, cx);
+ }
}
- pub fn query(&self, cx: &AppContext) -> String {
- self.query_editor.read(cx).text(cx)
+ fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+ if self.pending_update_matches.is_some() {
+ self.confirm_on_update = Some(true)
+ } else {
+ self.delegate.confirm(true, cx);
+ }
}
- pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
- self.query_editor
- .update(cx, |editor, cx| editor.set_text(query, cx));
+ fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
+ cx.stop_propagation();
+ cx.prevent_default();
+ self.delegate.set_selected_index(ix, cx);
+ self.delegate.confirm(secondary, cx);
}
- fn on_query_editor_event(
+ fn on_input_editor_event(
&mut self,
- _: ViewHandle<Editor>,
- event: &editor::Event,
+ _: View<Editor>,
+ event: &editor::EditorEvent,
cx: &mut ViewContext<Self>,
) {
match event {
- editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx),
- editor::Event::Blurred if !self.confirmed => {
- self.dismiss(cx);
+ editor::EditorEvent::BufferEdited => {
+ let query = self.editor.read(cx).text(cx);
+ self.update_matches(query, cx);
+ }
+ editor::EditorEvent::Blurred => {
+ self.cancel(&menu::Cancel, cx);
}
_ => {}
}
}
+ pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
+ let query = self.editor.read(cx).text(cx);
+ self.update_matches(query, cx);
+ }
+
pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
let update = self.delegate.update_matches(query, cx);
self.matches_updated(cx);
@@ -270,99 +207,117 @@ impl<D: PickerDelegate> Picker<D> {
this.update(&mut cx, |this, cx| {
this.matches_updated(cx);
})
- .log_err()
+ .ok();
}));
}
fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
let index = self.delegate.selected_index();
- let target = if self.delegate.center_selection_after_match_updates() {
- ScrollTarget::Center(index)
- } else {
- ScrollTarget::Show(index)
- };
- self.list_state.scroll_to(target);
+ self.scroll_handle.scroll_to_item(index);
self.pending_update_matches = None;
if let Some(secondary) = self.confirm_on_update.take() {
- self.confirmed = true;
- self.delegate.confirm(secondary, cx)
+ self.delegate.confirm(secondary, cx);
}
cx.notify();
}
- pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
- if self.delegate.match_count() > 0 {
- self.delegate.set_selected_index(0, cx);
- self.list_state.scroll_to(ScrollTarget::Show(0));
- }
-
- cx.notify();
- }
-
- pub fn select_index(&mut self, index: usize, cmd: bool, cx: &mut ViewContext<Self>) {
- if self.delegate.match_count() > 0 {
- self.confirmed = true;
- self.delegate.set_selected_index(index, cx);
- self.delegate.confirm(cmd, cx);
- }
- }
-
- pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
- let match_count = self.delegate.match_count();
- if match_count > 0 {
- let index = match_count - 1;
- self.delegate.set_selected_index(index, cx);
- self.list_state.scroll_to(ScrollTarget::Show(index));
- }
- cx.notify();
- }
-
- pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- let next_index = self.delegate.selected_index() + 1;
- if next_index < self.delegate.match_count() {
- self.delegate.set_selected_index(next_index, cx);
- self.list_state.scroll_to(ScrollTarget::Show(next_index));
- }
-
- cx.notify();
- }
-
- pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- let mut selected_index = self.delegate.selected_index();
- if selected_index > 0 {
- selected_index -= 1;
- self.delegate.set_selected_index(selected_index, cx);
- self.list_state
- .scroll_to(ScrollTarget::Show(selected_index));
- }
-
- cx.notify();
- }
-
- pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
- if self.pending_update_matches.is_some() {
- self.confirm_on_update = Some(false)
- } else {
- self.confirmed = true;
- self.delegate.confirm(false, cx);
- }
+ pub fn query(&self, cx: &AppContext) -> String {
+ self.editor.read(cx).text(cx)
}
- pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
- if self.pending_update_matches.is_some() {
- self.confirm_on_update = Some(true)
- } else {
- self.confirmed = true;
- self.delegate.confirm(true, cx);
- }
+ pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| editor.set_text(query, cx));
}
+}
- fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
- self.dismiss(cx);
- }
+impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
+impl<D: PickerDelegate> ModalView for Picker<D> {}
+
+impl<D: PickerDelegate> Render for Picker<D> {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let picker_editor = h_stack()
+ .overflow_hidden()
+ .flex_none()
+ .h_9()
+ .px_4()
+ .child(self.editor.clone());
+
+ div()
+ .key_context("Picker")
+ .size_full()
+ .when_some(self.width, |el, width| el.w(width))
+ .overflow_hidden()
+ // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
+ // as a part of a modal rather than the entire modal.
+ //
+ // We should revisit how the `Picker` is styled to make it more composable.
+ .when(self.is_modal, |this| this.elevation_3(cx))
+ .on_action(cx.listener(Self::select_next))
+ .on_action(cx.listener(Self::select_prev))
+ .on_action(cx.listener(Self::select_first))
+ .on_action(cx.listener(Self::select_last))
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::confirm))
+ .on_action(cx.listener(Self::secondary_confirm))
+ .child(picker_editor)
+ .child(Divider::horizontal())
+ .when(self.delegate.match_count() > 0, |el| {
+ el.child(
+ v_stack()
+ .flex_grow()
+ .py_2()
+ .max_h(self.max_height.unwrap_or(rems(18.).into()))
+ .overflow_hidden()
+ .children(self.delegate.render_header(cx))
+ .child(
+ uniform_list(
+ cx.view().clone(),
+ "candidates",
+ self.delegate.match_count(),
+ {
+ let separators_after_indices = self.delegate.separators_after_indices();
+ let selected_index = self.delegate.selected_index();
+ move |picker, visible_range, cx| {
+ visible_range
+ .map(|ix| {
+ div()
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |this, event: &MouseDownEvent, cx| {
+ this.handle_click(
+ ix,
+ event.modifiers.command,
+ cx,
+ )
+ }),
+ )
+ .children(picker.delegate.render_match(
+ ix,
+ ix == selected_index,
+ cx,
+ )).when(separators_after_indices.contains(&ix), |picker| picker.child(ListSeparator))
+ })
+ .collect()
+ }
+ },
+ )
+ .track_scroll(self.scroll_handle.clone())
+ )
- fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
- cx.emit(PickerEvent::Dismiss);
- self.delegate.dismissed(cx);
+ )
+ })
+ .when(self.delegate.match_count() == 0, |el| {
+ el.child(
+ v_stack().flex_grow().py_2().child(
+ ListItem::new("empty_state")
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .disabled(true)
+ .child(Label::new("No matches").color(Color::Muted)),
+ ),
+ )
+ })
+ .children(self.delegate.render_footer(cx))
}
}
@@ -1,28 +0,0 @@
-[package]
-name = "picker2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/picker2.rs"
-doctest = false
-
-[dependencies]
-editor = { package = "editor2", path = "../editor2" }
-ui = { package = "ui2", path = "../ui2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-menu = { package = "menu2", path = "../menu2" }
-settings = { package = "settings2", path = "../settings2" }
-util = { path = "../util" }
-theme = { package = "theme2", path = "../theme2" }
-workspace = { package = "workspace2", path = "../workspace2"}
-
-parking_lot.workspace = true
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
-gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-serde_json.workspace = true
-ctor.workspace = true
-env_logger.workspace = true
@@ -1,323 +0,0 @@
-use editor::Editor;
-use gpui::{
- div, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
- FocusableView, Length, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle,
- View, ViewContext, WindowContext,
-};
-use std::{cmp, sync::Arc};
-use ui::{prelude::*, v_stack, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator};
-use workspace::ModalView;
-
-pub struct Picker<D: PickerDelegate> {
- pub delegate: D,
- scroll_handle: UniformListScrollHandle,
- editor: View<Editor>,
- pending_update_matches: Option<Task<()>>,
- confirm_on_update: Option<bool>,
- width: Option<Length>,
- max_height: Option<Length>,
-
- /// Whether the `Picker` is rendered as a self-contained modal.
- ///
- /// Set this to `false` when rendering the `Picker` as part of a larger modal.
- is_modal: bool,
-}
-
-pub trait PickerDelegate: Sized + 'static {
- type ListItem: IntoElement;
- fn match_count(&self) -> usize;
- fn selected_index(&self) -> usize;
- fn separators_after_indices(&self) -> Vec<usize> {
- Vec::new()
- }
- fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
-
- fn placeholder_text(&self) -> Arc<str>;
- fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
-
- fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
- fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<Self::ListItem>;
- fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
- None
- }
- fn render_footer(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
- None
- }
-}
-
-impl<D: PickerDelegate> FocusableView for Picker<D> {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.editor.focus_handle(cx)
- }
-}
-
-impl<D: PickerDelegate> Picker<D> {
- pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
- let editor = cx.new_view(|cx| {
- let mut editor = Editor::single_line(cx);
- editor.set_placeholder_text(delegate.placeholder_text(), cx);
- editor
- });
- cx.subscribe(&editor, Self::on_input_editor_event).detach();
- let mut this = Self {
- delegate,
- editor,
- scroll_handle: UniformListScrollHandle::new(),
- pending_update_matches: None,
- confirm_on_update: None,
- width: None,
- max_height: None,
- is_modal: true,
- };
- this.update_matches("".to_string(), cx);
- this
- }
-
- pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
- self.width = Some(width.into());
- self
- }
-
- pub fn max_height(mut self, max_height: impl Into<gpui::Length>) -> Self {
- self.max_height = Some(max_height.into());
- self
- }
-
- pub fn modal(mut self, modal: bool) -> Self {
- self.is_modal = modal;
- self
- }
-
- pub fn focus(&self, cx: &mut WindowContext) {
- self.editor.update(cx, |editor, cx| editor.focus(cx));
- }
-
- pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
- let count = self.delegate.match_count();
- if count > 0 {
- let index = self.delegate.selected_index();
- let ix = cmp::min(index + 1, count - 1);
- self.delegate.set_selected_index(ix, cx);
- self.scroll_handle.scroll_to_item(ix);
- cx.notify();
- }
- }
-
- fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
- let count = self.delegate.match_count();
- if count > 0 {
- let index = self.delegate.selected_index();
- let ix = index.saturating_sub(1);
- self.delegate.set_selected_index(ix, cx);
- self.scroll_handle.scroll_to_item(ix);
- cx.notify();
- }
- }
-
- fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
- let count = self.delegate.match_count();
- if count > 0 {
- self.delegate.set_selected_index(0, cx);
- self.scroll_handle.scroll_to_item(0);
- cx.notify();
- }
- }
-
- fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
- let count = self.delegate.match_count();
- if count > 0 {
- self.delegate.set_selected_index(count - 1, cx);
- self.scroll_handle.scroll_to_item(count - 1);
- cx.notify();
- }
- }
-
- pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
- let count = self.delegate.match_count();
- let index = self.delegate.selected_index();
- let new_index = if index + 1 == count { 0 } else { index + 1 };
- self.delegate.set_selected_index(new_index, cx);
- self.scroll_handle.scroll_to_item(new_index);
- cx.notify();
- }
-
- pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- self.delegate.dismissed(cx);
- cx.emit(DismissEvent);
- }
-
- fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
- if self.pending_update_matches.is_some() {
- self.confirm_on_update = Some(false)
- } else {
- self.delegate.confirm(false, cx);
- }
- }
-
- fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
- if self.pending_update_matches.is_some() {
- self.confirm_on_update = Some(true)
- } else {
- self.delegate.confirm(true, cx);
- }
- }
-
- fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
- cx.stop_propagation();
- cx.prevent_default();
- self.delegate.set_selected_index(ix, cx);
- self.delegate.confirm(secondary, cx);
- }
-
- fn on_input_editor_event(
- &mut self,
- _: View<Editor>,
- event: &editor::EditorEvent,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- editor::EditorEvent::BufferEdited => {
- let query = self.editor.read(cx).text(cx);
- self.update_matches(query, cx);
- }
- editor::EditorEvent::Blurred => {
- self.cancel(&menu::Cancel, cx);
- }
- _ => {}
- }
- }
-
- pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
- let query = self.editor.read(cx).text(cx);
- self.update_matches(query, cx);
- }
-
- pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
- let update = self.delegate.update_matches(query, cx);
- self.matches_updated(cx);
- self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
- update.await;
- this.update(&mut cx, |this, cx| {
- this.matches_updated(cx);
- })
- .ok();
- }));
- }
-
- fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
- let index = self.delegate.selected_index();
- self.scroll_handle.scroll_to_item(index);
- self.pending_update_matches = None;
- if let Some(secondary) = self.confirm_on_update.take() {
- self.delegate.confirm(secondary, cx);
- }
- cx.notify();
- }
-
- pub fn query(&self, cx: &AppContext) -> String {
- self.editor.read(cx).text(cx)
- }
-
- pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| editor.set_text(query, cx));
- }
-}
-
-impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
-impl<D: PickerDelegate> ModalView for Picker<D> {}
-
-impl<D: PickerDelegate> Render for Picker<D> {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let picker_editor = h_stack()
- .overflow_hidden()
- .flex_none()
- .h_9()
- .px_4()
- .child(self.editor.clone());
-
- div()
- .key_context("Picker")
- .size_full()
- .when_some(self.width, |el, width| el.w(width))
- .overflow_hidden()
- // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
- // as a part of a modal rather than the entire modal.
- //
- // We should revisit how the `Picker` is styled to make it more composable.
- .when(self.is_modal, |this| this.elevation_3(cx))
- .on_action(cx.listener(Self::select_next))
- .on_action(cx.listener(Self::select_prev))
- .on_action(cx.listener(Self::select_first))
- .on_action(cx.listener(Self::select_last))
- .on_action(cx.listener(Self::cancel))
- .on_action(cx.listener(Self::confirm))
- .on_action(cx.listener(Self::secondary_confirm))
- .child(picker_editor)
- .child(Divider::horizontal())
- .when(self.delegate.match_count() > 0, |el| {
- el.child(
- v_stack()
- .flex_grow()
- .py_2()
- .max_h(self.max_height.unwrap_or(rems(18.).into()))
- .overflow_hidden()
- .children(self.delegate.render_header(cx))
- .child(
- uniform_list(
- cx.view().clone(),
- "candidates",
- self.delegate.match_count(),
- {
- let separators_after_indices = self.delegate.separators_after_indices();
- let selected_index = self.delegate.selected_index();
- move |picker, visible_range, cx| {
- visible_range
- .map(|ix| {
- div()
- .on_mouse_down(
- MouseButton::Left,
- cx.listener(move |this, event: &MouseDownEvent, cx| {
- this.handle_click(
- ix,
- event.modifiers.command,
- cx,
- )
- }),
- )
- .children(picker.delegate.render_match(
- ix,
- ix == selected_index,
- cx,
- )).when(separators_after_indices.contains(&ix), |picker| picker.child(ListSeparator))
- })
- .collect()
- }
- },
- )
- .track_scroll(self.scroll_handle.clone())
- )
-
- )
- })
- .when(self.delegate.match_count() == 0, |el| {
- el.child(
- v_stack().flex_grow().py_2().child(
- ListItem::new("empty_state")
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .disabled(true)
- .child(Label::new("No matches").color(Color::Muted)),
- ),
- )
- })
- .children(self.delegate.render_footer(cx))
- }
-}
@@ -12,7 +12,7 @@ doctest = false
editor = { package = "editor2", path = "../editor2" }
fuzzy = {package = "fuzzy2", path = "../fuzzy2" }
gpui = {package = "gpui2", path = "../gpui2" }
-picker = {package = "picker2", path = "../picker2" }
+picker = {path = "../picker" }
project = { package = "project2", path = "../project2" }
text = {package = "text2", path = "../text2" }
settings = {package = "settings2", path = "../settings2" }
@@ -13,7 +13,7 @@ editor = { package = "editor2", path = "../editor2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
settings = { package = "settings2", path = "../settings2" }
text = { package = "text2", path = "../text2" }
util = { path = "../util"}
@@ -34,7 +34,7 @@ theme2 = { path = "../theme2" }
menu = { package = "menu2", path = "../menu2" }
ui = { package = "ui2", path = "../ui2", features = ["stories"] }
util = { path = "../util" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
[dev-dependencies]
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
@@ -15,7 +15,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
fs = { package = "fs2", path = "../fs2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
@@ -9,7 +9,7 @@ publish = false
fuzzy = {package = "fuzzy2", path = "../fuzzy2"}
fs = {package = "fs2", path = "../fs2"}
gpui = {package = "gpui2", path = "../gpui2"}
-picker = {package = "picker2", path = "../picker2"}
+picker = {path = "../picker"}
util = {path = "../util"}
ui = {package = "ui2", path = "../ui2"}
workspace = {package = "workspace2", path = "../workspace2"}
@@ -24,7 +24,7 @@ settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
workspace = { package = "workspace2", path = "../workspace2" }
vim = { path = "../vim" }
@@ -34,7 +34,7 @@ copilot_button = { path = "../copilot_button" }
diagnostics = { path = "../diagnostics" }
db = { package = "db2", path = "../db2" }
editor = { package="editor2", path = "../editor2" }
-feedback = { package="feedback2", path = "../feedback2" }
+feedback = { path = "../feedback" }
file_finder = { path = "../file_finder" }
search = { path = "../search" }
fs = { package = "fs2", path = "../fs2" }
@@ -150,7 +150,7 @@ pub fn app_menus() -> Vec<Menu<'static>> {
MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
MenuItem::action("Show Welcome", workspace::Welcome),
MenuItem::separator(),
- // todo!(): Needs `feedback2` crate.
+ // todo!(): Needs `feedback` crate.
// MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback),
// MenuItem::action(
// "Copy System Specs Into Clipboard",