zeta: Revised data-collection onboarding experience (#24031)
Release Notes: - N/A --------- Co-authored-by: Danilo <danilo@zed.dev> Co-authored-by: Danilo Leal <daniloleal09@gmail.com> Co-authored-by: João Marcos <marcospb19@hotmail.com>
This commit is contained in:
parent
29e559d60c
commit
93f8ccaaee
31 changed files with 760 additions and 601 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -4059,7 +4059,7 @@ dependencies = [
|
|||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
"zed_predict_onboarding",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6454,7 +6454,6 @@ dependencies = [
|
|||
"ui",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_onboarding",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
|
@ -13590,7 +13589,7 @@ dependencies = [
|
|||
"windows 0.58.0",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_onboarding",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -16588,7 +16587,6 @@ dependencies = [
|
|||
"winresource",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_onboarding",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
|
@ -16702,25 +16700,6 @@ dependencies = [
|
|||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_predict_onboarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"client",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_proto"
|
||||
version = "0.2.1"
|
||||
|
@ -16906,6 +16885,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"arrayvec",
|
||||
"call",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
|
@ -16915,6 +16895,7 @@ dependencies = [
|
|||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
|
@ -16924,6 +16905,8 @@ dependencies = [
|
|||
"language_models",
|
||||
"log",
|
||||
"menu",
|
||||
"postage",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
"rpc",
|
||||
"serde",
|
||||
|
@ -16936,10 +16919,12 @@ dependencies = [
|
|||
"tree-sitter-go",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
"worktree",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -152,7 +152,6 @@ members = [
|
|||
"crates/worktree",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_predict_onboarding",
|
||||
"crates/zeta",
|
||||
|
||||
#
|
||||
|
@ -348,7 +347,6 @@ workspace = { path = "crates/workspace" }
|
|||
worktree = { path = "crates/worktree" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zed_predict_onboarding = { path = "crates/zed_predict_onboarding" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
|
||||
#
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<svg width="420" height="128" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="440" height="128" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="tilePattern" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
Before Width: | Height: | Size: 971 B After Width: | Height: | Size: 971 B |
|
@ -87,7 +87,7 @@ url.workspace = true
|
|||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
|
|
@ -69,7 +69,6 @@ pub use element::{
|
|||
};
|
||||
use futures::{future, FutureExt};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use zed_predict_onboarding::ZedPredictModal;
|
||||
|
||||
use code_context_menus::{
|
||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||
|
@ -617,7 +616,8 @@ pub struct Editor {
|
|||
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
||||
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
|
||||
|
||||
project: Option<Entity<Project>>,
|
||||
// TODO: make this a access method
|
||||
pub project: Option<Entity<Project>>,
|
||||
semantics_provider: Option<Rc<dyn SemanticsProvider>>,
|
||||
completion_provider: Option<Box<dyn CompletionProvider>>,
|
||||
collaboration_hub: Option<Box<dyn CollaborationHub>>,
|
||||
|
@ -3944,20 +3944,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
fn toggle_zed_predict_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let (Some(workspace), Some(project)) = (self.workspace(), self.project.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = project.read(cx);
|
||||
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
project.user_store().clone(),
|
||||
project.client().clone(),
|
||||
project.fs().clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx);
|
||||
}
|
||||
|
||||
fn do_completion(
|
||||
|
|
|
@ -21,8 +21,6 @@ pub struct InlineCompletion {
|
|||
pub enum DataCollectionState {
|
||||
/// The provider doesn't support data collection.
|
||||
Unsupported,
|
||||
/// When there's a file not saved yet. In this case, we can't tell to which project it belongs.
|
||||
Unknown,
|
||||
/// Data collection is enabled
|
||||
Enabled,
|
||||
/// Data collection is disabled or unanswered.
|
||||
|
@ -34,10 +32,6 @@ impl DataCollectionState {
|
|||
!matches!(self, DataCollectionState::Unsupported)
|
||||
}
|
||||
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
matches!(self, DataCollectionState::Unknown)
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(self, DataCollectionState::Enabled)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ workspace.workspace = true
|
|||
zed_actions.workspace = true
|
||||
zeta.workspace = true
|
||||
client.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
copilot = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use client::{Client, UserStore};
|
||||
use client::UserStore;
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{actions::ShowInlineCompletion, scroll::Autoscroll, Editor};
|
||||
use feature_flags::{
|
||||
|
@ -21,15 +21,14 @@ use settings::{update_settings_file, Settings, SettingsStore};
|
|||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, Clickable, ContextMenu, ContextMenuEntry, IconButton,
|
||||
IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*, Clickable, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, PopoverMenu,
|
||||
PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use workspace::{
|
||||
create_and_open_local_file, item::ItemHandle, notifications::NotificationId, StatusItemView,
|
||||
Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_predict_onboarding::ZedPredictModal;
|
||||
use zeta::RateCompletionModal;
|
||||
|
||||
actions!(zeta, [RateCompletions]);
|
||||
|
@ -46,7 +45,6 @@ pub struct InlineCompletionButton {
|
|||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
|
@ -230,71 +228,49 @@ impl Render for InlineCompletionButton {
|
|||
return div();
|
||||
}
|
||||
|
||||
fn icon_button() -> IconButton {
|
||||
IconButton::new("zed-predict-pending-button", IconName::ZedPredict)
|
||||
.shape(IconButtonShape::Square)
|
||||
}
|
||||
|
||||
let current_user_terms_accepted =
|
||||
self.user_store.read(cx).current_user_has_accepted_terms();
|
||||
|
||||
if !current_user_terms_accepted.unwrap_or(false) {
|
||||
let workspace = self.workspace.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let client = self.client.clone();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
let signed_in = current_user_terms_accepted.is_some();
|
||||
let tooltip_meta = if signed_in {
|
||||
"Read Terms of Service"
|
||||
} else {
|
||||
"Sign in to use"
|
||||
};
|
||||
|
||||
return div().child(
|
||||
ButtonLike::new("zeta-pending-tos-icon")
|
||||
.child(
|
||||
IconWithIndicator::new(
|
||||
Icon::new(IconName::ZedPredict),
|
||||
Some(Indicator::dot().color(Color::Error)),
|
||||
)
|
||||
.indicator_border_color(Some(
|
||||
cx.theme().colors().status_bar_background,
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
icon_button()
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Edit Predictions",
|
||||
None,
|
||||
if signed_in {
|
||||
"Read Terms of Service"
|
||||
} else {
|
||||
"Sign in to use"
|
||||
},
|
||||
tooltip_meta,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
window.dispatch_action(
|
||||
zed_actions::OpenZedPredictOnboarding.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
let this = cx.entity().clone();
|
||||
let button = IconButton::new("zeta", IconName::ZedPredict).when(
|
||||
!self.popover_menu_handle.is_deployed(),
|
||||
|button| {
|
||||
button.tooltip(|window, cx| {
|
||||
Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let is_refreshing = self
|
||||
.inline_completion_provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.is_refreshing(cx));
|
||||
if !self.popover_menu_handle.is_deployed() {
|
||||
icon_button().tooltip(|window, cx| {
|
||||
Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
let mut popover_menu = PopoverMenu::new("zeta")
|
||||
.menu(move |window, cx| {
|
||||
|
@ -303,9 +279,14 @@ impl Render for InlineCompletionButton {
|
|||
.anchor(Corner::BottomRight)
|
||||
.with_handle(self.popover_menu_handle.clone());
|
||||
|
||||
let is_refreshing = self
|
||||
.inline_completion_provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.is_refreshing(cx));
|
||||
|
||||
if is_refreshing {
|
||||
popover_menu = popover_menu.trigger(
|
||||
button.with_animation(
|
||||
icon_button().with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
|
@ -314,7 +295,7 @@ impl Render for InlineCompletionButton {
|
|||
),
|
||||
);
|
||||
} else {
|
||||
popover_menu = popover_menu.trigger(button);
|
||||
popover_menu = popover_menu.trigger(icon_button());
|
||||
}
|
||||
|
||||
div().child(popover_menu.into_any_element())
|
||||
|
@ -328,7 +309,6 @@ impl InlineCompletionButton {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
fs: Arc<dyn Fs>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
|
@ -348,7 +328,6 @@ impl InlineCompletionButton {
|
|||
inline_completion_provider: None,
|
||||
popover_menu_handle,
|
||||
workspace,
|
||||
client,
|
||||
fs,
|
||||
user_store,
|
||||
}
|
||||
|
@ -447,10 +426,15 @@ impl InlineCompletionButton {
|
|||
|
||||
if data_collection.is_supported() {
|
||||
let provider = provider.clone();
|
||||
menu = menu.separator().item(
|
||||
ContextMenuEntry::new("Data Collection")
|
||||
menu = menu
|
||||
.separator()
|
||||
.header("Help Improve The Model")
|
||||
.header("For OSS Projects Only");
|
||||
menu = menu.item(
|
||||
// TODO: We want to add something later that communicates whether
|
||||
// the current project is open-source.
|
||||
ContextMenuEntry::new("Share Training Data")
|
||||
.toggleable(IconPosition::Start, data_collection.is_enabled())
|
||||
.disabled(data_collection.is_unknown())
|
||||
.handler(move |_, cx| {
|
||||
provider.toggle_data_collection(cx);
|
||||
}),
|
||||
|
|
|
@ -41,7 +41,7 @@ pub struct PredictEditsParams {
|
|||
pub input_excerpt: String,
|
||||
/// Whether the user provided consent for sampling this interaction.
|
||||
#[serde(default)]
|
||||
pub can_collect_data: bool,
|
||||
pub data_collection_permission: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -41,13 +41,13 @@ serde.workspace = true
|
|||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
telemetry.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
zeta.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
|
|
@ -34,7 +34,7 @@ use ui::{
|
|||
use util::ResultExt;
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
|
||||
use zed_predict_onboarding::ZedPredictBanner;
|
||||
use zeta::ZedPredictBanner;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
@ -162,6 +162,7 @@ impl Render for TitleBar {
|
|||
.id("titlebar-content")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
// Note: On Windows the title bar behavior is handled by the platform implementation.
|
||||
|
@ -268,7 +269,6 @@ impl TitleBar {
|
|||
let project = workspace.project().clone();
|
||||
let user_store = workspace.app_state().user_store.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
let platform_style = PlatformStyle::platform();
|
||||
|
@ -296,15 +296,7 @@ impl TitleBar {
|
|||
subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
|
||||
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
|
||||
|
||||
let zed_predict_banner = cx.new(|cx| {
|
||||
ZedPredictBanner::new(
|
||||
workspace.weak_handle(),
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let zed_predict_banner = cx.new(ZedPredictBanner::new);
|
||||
|
||||
Self {
|
||||
platform_style,
|
||||
|
|
|
@ -385,6 +385,11 @@ impl ButtonLike {
|
|||
Self::new(id).rounding(ButtonLikeRounding::Right)
|
||||
}
|
||||
|
||||
pub fn opacity(mut self, opacity: f32) -> Self {
|
||||
self.base = self.base.opacity(opacity);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
|
||||
self.height = Some(height);
|
||||
self
|
||||
|
|
|
@ -57,12 +57,19 @@ impl<M> Default for PopoverMenuHandle<M> {
|
|||
struct PopoverMenuHandleState<M> {
|
||||
menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
|
||||
menu: Rc<RefCell<Option<Entity<M>>>>,
|
||||
on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl<M: ManagedView> PopoverMenuHandle<M> {
|
||||
pub fn show(&self, window: &mut Window, cx: &mut App) {
|
||||
if let Some(state) = self.0.borrow().as_ref() {
|
||||
show_menu(&state.menu_builder, &state.menu, window, cx);
|
||||
show_menu(
|
||||
&state.menu_builder,
|
||||
&state.menu,
|
||||
state.on_open.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,6 +125,7 @@ pub struct PopoverMenu<M: ManagedView> {
|
|||
attach: Option<Corner>,
|
||||
offset: Option<Point<Pixels>>,
|
||||
trigger_handle: Option<PopoverMenuHandle<M>>,
|
||||
on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
|
||||
full_width: bool,
|
||||
}
|
||||
|
||||
|
@ -132,6 +140,7 @@ impl<M: ManagedView> PopoverMenu<M> {
|
|||
attach: None,
|
||||
offset: None,
|
||||
trigger_handle: None,
|
||||
on_open: None,
|
||||
full_width: false,
|
||||
}
|
||||
}
|
||||
|
@ -155,11 +164,14 @@ impl<M: ManagedView> PopoverMenu<M> {
|
|||
}
|
||||
|
||||
pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
|
||||
self.child_builder = Some(Box::new(|menu, builder| {
|
||||
let on_open = self.on_open.clone();
|
||||
self.child_builder = Some(Box::new(move |menu, builder| {
|
||||
let open = menu.borrow().is_some();
|
||||
t.toggle_state(open)
|
||||
.when_some(builder, |el, builder| {
|
||||
el.on_click(move |_event, window, cx| show_menu(&builder, &menu, window, cx))
|
||||
el.on_click(move |_event, window, cx| {
|
||||
show_menu(&builder, &menu, on_open.clone(), window, cx)
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
}));
|
||||
|
@ -185,6 +197,12 @@ impl<M: ManagedView> PopoverMenu<M> {
|
|||
self
|
||||
}
|
||||
|
||||
/// attach something upon opening the menu
|
||||
pub fn on_open(mut self, on_open: Rc<dyn Fn(&mut Window, &mut App)>) -> Self {
|
||||
self.on_open = Some(on_open);
|
||||
self
|
||||
}
|
||||
|
||||
fn resolved_attach(&self) -> Corner {
|
||||
self.attach.unwrap_or(match self.anchor {
|
||||
Corner::TopLeft => Corner::BottomLeft,
|
||||
|
@ -209,6 +227,7 @@ impl<M: ManagedView> PopoverMenu<M> {
|
|||
fn show_menu<M: ManagedView>(
|
||||
builder: &Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
|
||||
menu: &Rc<RefCell<Option<Entity<M>>>>,
|
||||
on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
|
@ -232,6 +251,10 @@ fn show_menu<M: ManagedView>(
|
|||
window.focus(&new_menu.focus_handle(cx));
|
||||
*menu.borrow_mut() = Some(new_menu);
|
||||
window.refresh();
|
||||
|
||||
if let Some(on_open) = on_open {
|
||||
on_open(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PopoverMenuElementState<M> {
|
||||
|
@ -311,6 +334,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
|
|||
*trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState {
|
||||
menu_builder,
|
||||
menu: element_state.menu.clone(),
|
||||
on_open: self.on_open.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use gpui::{div, hsla, prelude::*, AnyView, ElementId, Hsla, IntoElement, Styled, Window};
|
||||
use gpui::{
|
||||
div, hsla, prelude::*, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::utils::is_light;
|
||||
|
@ -45,6 +47,7 @@ pub struct Checkbox {
|
|||
filled: bool,
|
||||
style: ToggleStyle,
|
||||
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
|
||||
label: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
|
@ -58,6 +61,7 @@ impl Checkbox {
|
|||
filled: false,
|
||||
style: ToggleStyle::default(),
|
||||
tooltip: None,
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,6 +103,12 @@ impl Checkbox {
|
|||
self.tooltip = Some(Box::new(tooltip));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the label for the checkbox.
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
|
@ -116,11 +126,11 @@ impl Checkbox {
|
|||
|
||||
fn border_color(&self, cx: &App) -> Hsla {
|
||||
if self.disabled {
|
||||
return cx.theme().colors().border_disabled;
|
||||
return cx.theme().colors().border_variant;
|
||||
}
|
||||
|
||||
match self.style.clone() {
|
||||
ToggleStyle::Ghost => cx.theme().colors().border_variant,
|
||||
ToggleStyle::Ghost => cx.theme().colors().border,
|
||||
ToggleStyle::ElevationBased(elevation) => elevation.on_elevation_bg(cx),
|
||||
ToggleStyle::Custom(color) => color.opacity(0.3),
|
||||
}
|
||||
|
@ -153,10 +163,8 @@ impl RenderOnce for Checkbox {
|
|||
let bg_color = self.bg_color(cx);
|
||||
let border_color = self.border_color(cx);
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
let checkbox = h_flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.size(DynamicSpacing::Base20.rems(cx))
|
||||
.group(group_id.clone())
|
||||
.child(
|
||||
|
@ -171,13 +179,24 @@ impl RenderOnce for Checkbox {
|
|||
.bg(bg_color)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.when(self.disabled, |this| {
|
||||
this.cursor(CursorStyle::OperationNotAllowed)
|
||||
})
|
||||
.when(self.disabled, |this| {
|
||||
this.bg(cx.theme().colors().element_disabled.opacity(0.6))
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.group_hover(group_id.clone(), |el| {
|
||||
el.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
})
|
||||
.children(icon),
|
||||
)
|
||||
);
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.gap(DynamicSpacing::Base06.rems(cx))
|
||||
.child(checkbox)
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| {
|
||||
|
@ -186,6 +205,11 @@ impl RenderOnce for Checkbox {
|
|||
})
|
||||
},
|
||||
)
|
||||
// TODO: Allow label size to be different from default.
|
||||
// TODO: Allow label color to be different from muted.
|
||||
.when_some(self.label, |this, label| {
|
||||
this.child(Label::new(label).color(Color::Muted))
|
||||
})
|
||||
.when_some(self.tooltip, |this, tooltip| {
|
||||
this.tooltip(move |window, cx| tooltip(window, cx))
|
||||
})
|
||||
|
@ -203,6 +227,7 @@ pub struct CheckboxWithLabel {
|
|||
style: ToggleStyle,
|
||||
}
|
||||
|
||||
// TODO: Remove `CheckboxWithLabel` now that `label` is a method of `Checkbox`.
|
||||
impl CheckboxWithLabel {
|
||||
/// Creates a checkbox with an attached label.
|
||||
pub fn new(
|
||||
|
|
|
@ -87,7 +87,7 @@ pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
|
|||
/// May correspond to a directory or a single file.
|
||||
/// Possible examples:
|
||||
/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree
|
||||
/// * a directory opened in Zed — may be added as a visible entry to the current worktree
|
||||
/// * a directory opened in Zed — may be added as a visible entry to the current worktree
|
||||
///
|
||||
/// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries.
|
||||
pub enum Worktree {
|
||||
|
|
|
@ -16,7 +16,6 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
activity_indicator.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant.workspace = true
|
||||
|
|
|
@ -439,7 +439,6 @@ fn main() {
|
|||
inline_completion_registry::init(
|
||||
app_state.client.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx);
|
||||
|
|
|
@ -176,7 +176,6 @@ pub fn initialize_workspace(
|
|||
workspace.weak_handle(),
|
||||
app_state.fs.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.client.clone(),
|
||||
popover_menu_handle.clone(),
|
||||
cx,
|
||||
)
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use copilot::{Copilot, CopilotCompletionProvider};
|
||||
use editor::{Editor, EditorMode};
|
||||
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Context, Entity, WeakEntity};
|
||||
use language::language_settings::{all_language_settings, InlineCompletionProvider};
|
||||
use settings::SettingsStore;
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
use supermaven::{Supermaven, SupermavenCompletionProvider};
|
||||
use ui::Window;
|
||||
use workspace::Workspace;
|
||||
use zed_predict_onboarding::ZedPredictModal;
|
||||
use zeta::ProviderDataCollection;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
let editors: Rc<RefCell<HashMap<WeakEntity<Editor>, AnyWindowHandle>>> = Rc::default();
|
||||
cx.observe_new({
|
||||
let editors = editors.clone();
|
||||
|
@ -96,7 +92,6 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>,
|
|||
let editors = editors.clone();
|
||||
let client = client.clone();
|
||||
let user_store = user_store.clone();
|
||||
let fs = fs.clone();
|
||||
move |cx| {
|
||||
let new_provider = all_language_settings(None, cx).inline_completions.provider;
|
||||
if new_provider != provider {
|
||||
|
@ -120,21 +115,10 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>,
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(Some(workspace)) = window
|
||||
.update(cx, |_, window, _| window.root().flatten())
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
window,
|
||||
window.dispatch_action(
|
||||
Box::new(zed_actions::OpenZedPredictOnboarding),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
|
@ -228,6 +212,7 @@ fn assign_inline_completion_provider(
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
// TODO: Do we really want to collect data only for singleton buffers?
|
||||
let singleton_buffer = editor.buffer().read(cx).as_singleton();
|
||||
|
||||
match provider {
|
||||
|
@ -255,7 +240,23 @@ fn assign_inline_completion_provider(
|
|||
if cx.has_flag::<PredictEditsFeatureFlag>()
|
||||
|| (cfg!(debug_assertions) && client.status().borrow().is_connected())
|
||||
{
|
||||
let zeta = zeta::Zeta::register(client.clone(), user_store, cx);
|
||||
let mut worktree = None;
|
||||
|
||||
if let Some(buffer) = &singleton_buffer {
|
||||
if let Some(file) = buffer.read(cx).file() {
|
||||
let id = file.worktree_id(cx);
|
||||
if let Some(inner_worktree) = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.and_then(|project| project.read(cx).worktree_for_id(id, cx))
|
||||
{
|
||||
worktree = Some(inner_worktree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let zeta = zeta::Zeta::register(worktree, client.clone(), user_store, cx);
|
||||
|
||||
if let Some(buffer) = &singleton_buffer {
|
||||
if buffer.read(cx).file().is_some() {
|
||||
zeta.update(cx, |zeta, cx| {
|
||||
|
@ -264,12 +265,8 @@ fn assign_inline_completion_provider(
|
|||
}
|
||||
}
|
||||
|
||||
let data_collection = ProviderDataCollection::new(
|
||||
zeta.clone(),
|
||||
window.root::<Workspace>().flatten(),
|
||||
singleton_buffer,
|
||||
cx,
|
||||
);
|
||||
let data_collection =
|
||||
ProviderDataCollection::new(zeta.clone(), singleton_buffer, cx);
|
||||
|
||||
let provider =
|
||||
cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta, data_collection));
|
||||
|
|
|
@ -186,3 +186,5 @@ pub mod outline {
|
|||
/// A pointer to outline::toggle function, exposed here to sewer the breadcrumbs <-> outline dependency.
|
||||
pub static TOGGLE_OUTLINE: OnceLock<fn(AnyView, &mut Window, &mut App)> = OnceLock::new();
|
||||
}
|
||||
|
||||
actions!(zed_predict_onboarding, [OpenZedPredictOnboarding]);
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
[package]
|
||||
name = "zed_predict_onboarding"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
|
@ -1 +0,0 @@
|
|||
../../LICENSE-GPL
|
|
@ -1,5 +0,0 @@
|
|||
mod banner;
|
||||
mod modal;
|
||||
|
||||
pub use banner::ZedPredictBanner;
|
||||
pub use modal::ZedPredictModal;
|
|
@ -19,12 +19,14 @@ test-support = []
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
arrayvec.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
|
@ -34,6 +36,8 @@ language.workspace = true
|
|||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
postage.workspace = true
|
||||
regex.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
@ -46,6 +50,8 @@ ui.workspace = true
|
|||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
worktree.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
|
@ -64,6 +70,7 @@ settings = { workspace = true, features = ["test-support"] }
|
|||
theme = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-go.workspace = true
|
||||
tree-sitter-rust.workspace = true
|
||||
unindent.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
call = { workspace = true, features = ["test-support"] }
|
||||
|
|
60
crates/zeta/src/init.rs
Normal file
60
crates/zeta/src/init.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use std::any::{Any, TypeId};
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{
|
||||
FeatureFlagAppExt as _, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag,
|
||||
};
|
||||
use ui::App;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{onboarding_modal::ZedPredictModal, RateCompletionModal, RateCompletions};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
|
||||
workspace.register_action(|workspace, _: &RateCompletions, window, cx| {
|
||||
if cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>() {
|
||||
RateCompletionModal::toggle(workspace, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(
|
||||
move |workspace, _: &zed_actions::OpenZedPredictOnboarding, window, cx| {
|
||||
if cx.has_flag::<PredictEditsFeatureFlag>() {
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
workspace.user_store().clone(),
|
||||
workspace.client().clone(),
|
||||
workspace.app_state().fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
|
||||
feature_gate_predict_edits_rating_actions(cx);
|
||||
}
|
||||
|
||||
fn feature_gate_predict_edits_rating_actions(cx: &mut App) {
|
||||
let rate_completion_action_types = [TypeId::of::<RateCompletions>()];
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&rate_completion_action_types);
|
||||
filter.hide_action_types(&[zed_actions::OpenZedPredictOnboarding.type_id()]);
|
||||
});
|
||||
|
||||
cx.observe_flag::<PredictEditsRateCompletionsFeatureFlag, _>(move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_action_types(rate_completion_action_types.iter());
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&rate_completion_action_types);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
210
crates/zeta/src/license_detection.rs
Normal file
210
crates/zeta/src/license_detection.rs
Normal file
|
@ -0,0 +1,210 @@
|
|||
use regex::Regex;
|
||||
|
||||
pub fn is_license_eligible_for_data_collection(license: &str) -> bool {
|
||||
// TODO: Include more licenses later (namely, Apache)
|
||||
for pattern in [MIT_LICENSE_REGEX, ISC_LICENSE_REGEX] {
|
||||
let regex = Regex::new(pattern.trim()).unwrap();
|
||||
if regex.is_match(license.trim()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
const MIT_LICENSE_REGEX: &str = r#"
|
||||
^.*MIT License.*
|
||||
|
||||
Copyright.*?
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files \(the "Software"\), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software\.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE\.$
|
||||
"#;
|
||||
|
||||
const ISC_LICENSE_REGEX: &str = r#"
|
||||
^ISC License
|
||||
|
||||
Copyright.*?
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies\.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS\. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\.$
|
||||
"#;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use unindent::unindent;
|
||||
|
||||
use crate::is_license_eligible_for_data_collection;
|
||||
|
||||
#[test]
|
||||
fn test_mit_positive_detection() {
|
||||
let example_license = unindent(
|
||||
r#"
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 John Doe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
|
||||
assert!(is_license_eligible_for_data_collection(&example_license));
|
||||
|
||||
let example_license = unindent(
|
||||
r#"
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 John Doe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
|
||||
assert!(is_license_eligible_for_data_collection(&example_license));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mit_negative_detection() {
|
||||
let example_license = unindent(
|
||||
r#"
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 John Doe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
This project is dual licensed under the MIT License and the Apache License, Version 2.0.
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
|
||||
assert!(!is_license_eligible_for_data_collection(&example_license));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isc_positive_detection() {
|
||||
let example_license = unindent(
|
||||
r#"
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2024, John Doe
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
|
||||
assert!(is_license_eligible_for_data_collection(&example_license));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isc_negative_detection() {
|
||||
let example_license = unindent(
|
||||
r#"
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2024, John Doe
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
This project is dual licensed under the ISC License and the MIT License.
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
|
||||
assert!(!is_license_eligible_for_data_collection(&example_license));
|
||||
}
|
||||
}
|
|
@ -1,40 +1,20 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::ZedPredictModal;
|
||||
use chrono::Utc;
|
||||
use client::{Client, UserStore};
|
||||
use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, Subscription, WeakEntity};
|
||||
use gpui::Subscription;
|
||||
use language::language_settings::{all_language_settings, InlineCompletionProvider};
|
||||
use settings::SettingsStore;
|
||||
use ui::{prelude::*, ButtonLike, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
/// Prompts user to try AI inline prediction feature
|
||||
/// Prompts the user to try Zed's Edit Prediction feature
|
||||
pub struct ZedPredictBanner {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
dismissed: bool,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ZedPredictBanner {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
user_store,
|
||||
client,
|
||||
fs,
|
||||
dismissed: get_dismissed(),
|
||||
_subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed),
|
||||
}
|
||||
|
@ -126,24 +106,8 @@ impl Render for ZedPredictBanner {
|
|||
.child(Label::new("Edit Prediction").size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let workspace = self.workspace.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let client = self.client.clone();
|
||||
let fs = self.fs.clone();
|
||||
move |_, window, cx| {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
|
@ -163,6 +127,6 @@ impl Render for ZedPredictBanner {
|
|||
),
|
||||
);
|
||||
|
||||
div().pr_1().child(banner)
|
||||
div().pr_2().child(banner)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::{Zeta, ZED_PREDICT_DATA_COLLECTION_CHOICE};
|
||||
use client::{Client, UserStore};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
|
@ -9,10 +11,12 @@ use gpui::{
|
|||
};
|
||||
use language::language_settings::{AllLanguageSettings, InlineCompletionProvider};
|
||||
use settings::{update_settings_file, Settings};
|
||||
use ui::{prelude::*, CheckboxWithLabel, TintColor};
|
||||
use ui::{prelude::*, Checkbox, TintColor, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
|
||||
use worktree::Worktree;
|
||||
|
||||
/// Introduces user to AI inline prediction feature and terms of service
|
||||
/// Introduces user to Zed's Edit Prediction feature and terms of service
|
||||
pub struct ZedPredictModal {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
|
@ -20,6 +24,9 @@ pub struct ZedPredictModal {
|
|||
focus_handle: FocusHandle,
|
||||
sign_in_status: SignInStatus,
|
||||
terms_of_service: bool,
|
||||
data_collection_expanded: bool,
|
||||
data_collection_opted_in: bool,
|
||||
worktrees: Vec<Entity<Worktree>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
|
@ -33,34 +40,26 @@ enum SignInStatus {
|
|||
}
|
||||
|
||||
impl ZedPredictModal {
|
||||
fn new(
|
||||
pub fn toggle(
|
||||
workspace: &mut Workspace,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
ZedPredictModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let worktrees = workspace.visible_worktrees(cx).collect();
|
||||
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
user_store,
|
||||
client,
|
||||
fs,
|
||||
focus_handle: cx.focus_handle(),
|
||||
sign_in_status: SignInStatus::Idle,
|
||||
terms_of_service: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(
|
||||
workspace: Entity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
workspace.update(cx, |this, cx| {
|
||||
this.toggle_modal(window, cx, |_window, cx| {
|
||||
ZedPredictModal::new(user_store, client, fs, cx)
|
||||
});
|
||||
data_collection_expanded: false,
|
||||
data_collection_opted_in: false,
|
||||
worktrees,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -74,6 +73,11 @@ impl ZedPredictModal {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn inline_completions_doc(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/docs/configuring-zed#inline-completions");
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = self
|
||||
.user_store
|
||||
|
@ -82,6 +86,20 @@ impl ZedPredictModal {
|
|||
cx.spawn(|this, mut cx| async move {
|
||||
task.await?;
|
||||
|
||||
let mut data_collection_opted_in = false;
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
data_collection_opted_in = this.data_collection_opted_in;
|
||||
})
|
||||
.ok();
|
||||
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
|
||||
data_collection_opted_in.to_string(),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| {
|
||||
file.features
|
||||
|
@ -89,6 +107,13 @@ impl ZedPredictModal {
|
|||
.inline_completion_provider = Some(InlineCompletionProvider::Zed);
|
||||
});
|
||||
|
||||
if this.worktrees.is_empty() {
|
||||
cx.emit(DismissEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
Zeta::register(None, this.client.clone(), this.user_store.clone(), cx);
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
|
@ -135,16 +160,16 @@ impl ModalView for ZedPredictModal {}
|
|||
impl Render for ZedPredictModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let base = v_flex()
|
||||
.w(px(420.))
|
||||
.id("zed predict tos")
|
||||
.key_context("ZedPredictModal")
|
||||
.w(px(440.))
|
||||
.p_4()
|
||||
.relative()
|
||||
.gap_2()
|
||||
.overflow_hidden()
|
||||
.elevation_3(cx)
|
||||
.id("zed predict tos")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.key_context("ZedPredictModal")
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
|
@ -155,15 +180,15 @@ impl Render for ZedPredictModal {
|
|||
div()
|
||||
.p_1p5()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.top_1()
|
||||
.left_1p5()
|
||||
.right_0()
|
||||
.h(px(200.))
|
||||
.child(
|
||||
svg()
|
||||
.path("icons/zed_predict_bg.svg")
|
||||
.text_color(cx.theme().colors().icon_disabled)
|
||||
.w(px(416.))
|
||||
.w(px(418.))
|
||||
.h(px(128.))
|
||||
.overflow_hidden(),
|
||||
),
|
||||
|
@ -249,24 +274,49 @@ impl Render for ZedPredictModal {
|
|||
|
||||
if self.user_store.read(cx).current_user().is_some() {
|
||||
let copy = match self.sign_in_status {
|
||||
SignInStatus::Idle => "Get accurate and helpful edit predictions at every keystroke. To set Zed as your inline completions provider, ensure you:",
|
||||
SignInStatus::Idle => "Get accurate and instant edit predictions at every keystroke. Before setting Zed as your inline completions provider:",
|
||||
SignInStatus::SignedIn => "Almost there! Ensure you:",
|
||||
SignInStatus::Waiting => unreachable!(),
|
||||
};
|
||||
|
||||
let accordion_icons = if self.data_collection_expanded {
|
||||
(IconName::ChevronUp, IconName::ChevronDown)
|
||||
} else {
|
||||
(IconName::ChevronDown, IconName::ChevronUp)
|
||||
};
|
||||
|
||||
fn label_item(label_text: impl Into<SharedString>) -> impl Element {
|
||||
Label::new(label_text).color(Color::Muted).into_element()
|
||||
}
|
||||
|
||||
fn info_item(label_text: impl Into<SharedString>) -> impl Element {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Check).size(IconSize::XSmall))
|
||||
.child(label_item(label_text))
|
||||
}
|
||||
|
||||
fn multiline_info_item<E1: Into<SharedString>, E2: IntoElement>(
|
||||
first_line: E1,
|
||||
second_line: E2,
|
||||
) -> impl Element {
|
||||
v_flex()
|
||||
.child(info_item(first_line))
|
||||
.child(div().pl_5().child(second_line))
|
||||
}
|
||||
|
||||
base.child(Label::new(copy).color(Color::Muted))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(CheckboxWithLabel::new(
|
||||
"tos-checkbox",
|
||||
Label::new("Have read and accepted the").color(Color::Muted),
|
||||
self.terms_of_service.into(),
|
||||
cx.listener(move |this, state, _window, cx| {
|
||||
this.terms_of_service = *state == ToggleState::Selected;
|
||||
cx.notify()
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
Checkbox::new("tos-checkbox", self.terms_of_service.into())
|
||||
.fill()
|
||||
.label("Read and accept the")
|
||||
.on_click(cx.listener(move |this, state, _window, cx| {
|
||||
this.terms_of_service = *state == ToggleState::Selected;
|
||||
cx.notify()
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("view-tos", "Terms of Service")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
|
@ -275,6 +325,88 @@ impl Render for ZedPredictModal {
|
|||
.on_click(cx.listener(Self::view_terms)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"training-data-checkbox",
|
||||
self.data_collection_opted_in.into(),
|
||||
)
|
||||
.label("Optionally share training data (OSS-only).")
|
||||
.fill()
|
||||
.when(self.worktrees.is_empty(), |element| {
|
||||
element.disabled(true).tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"No Project Open",
|
||||
None,
|
||||
"Open a project to enable this option.",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.on_click(cx.listener(
|
||||
move |this, state, _window, cx| {
|
||||
this.data_collection_opted_in =
|
||||
*state == ToggleState::Selected;
|
||||
cx.notify()
|
||||
},
|
||||
)),
|
||||
)
|
||||
// TODO: show each worktree if more than 1
|
||||
.child(
|
||||
Button::new("learn-more", "Learn More")
|
||||
.icon(accordion_icons.0)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.data_collection_expanded =
|
||||
!this.data_collection_expanded;
|
||||
cx.notify()
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(self.data_collection_expanded, |element| {
|
||||
element.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background.opacity(0.5))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
div().child(
|
||||
Label::new("To improve edit predictions, help fine-tune Zed's model by sharing data from the open-source projects you work on.")
|
||||
.mb_1()
|
||||
)
|
||||
)
|
||||
.child(info_item(
|
||||
"We ask this exclusively for open-source projects.",
|
||||
))
|
||||
.child(info_item(
|
||||
"Zed automatically detects if your project is open-source.",
|
||||
))
|
||||
.child(info_item(
|
||||
"This setting is valid for all OSS projects you open in Zed.",
|
||||
))
|
||||
.child(info_item("Toggle it anytime via the status bar menu."))
|
||||
.child(multiline_info_item(
|
||||
"Files that can contain sensitive data, like `.env`, are",
|
||||
h_flex()
|
||||
.child(label_item("excluded by default via the"))
|
||||
.child(
|
||||
Button::new("doc-link", "disabled_globs").on_click(
|
||||
cx.listener(Self::inline_completions_doc),
|
||||
),
|
||||
)
|
||||
.child(label_item("setting.")),
|
||||
)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
|
@ -1,48 +0,0 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
|
||||
define_connection!(
|
||||
pub static ref DB: ZetaDb<WorkspaceDb> = &[
|
||||
sql! (
|
||||
CREATE TABLE zeta_preferences(
|
||||
worktree_path BLOB NOT NULL PRIMARY KEY,
|
||||
accepted_data_collection INTEGER
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
);
|
||||
|
||||
impl ZetaDb {
|
||||
query! {
|
||||
pub fn get_all_data_collection_preferences() -> Result<Vec<(PathBuf, bool)>> {
|
||||
SELECT worktree_path, accepted_data_collection FROM zeta_preferences
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn get_accepted_data_collection(worktree_path: &Path) -> Result<Option<bool>> {
|
||||
SELECT accepted_data_collection FROM zeta_preferences
|
||||
WHERE worktree_path = ?
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn save_data_collection_choice(worktree_path: PathBuf, accepted_data_collection: bool) -> Result<()> {
|
||||
INSERT INTO zeta_preferences
|
||||
(worktree_path, accepted_data_collection)
|
||||
VALUES
|
||||
(?1, ?2)
|
||||
ON CONFLICT (worktree_path) DO UPDATE SET
|
||||
accepted_data_collection = ?2
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn clear_all_zeta_preferences() -> Result<()> {
|
||||
DELETE FROM zeta_preferences
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use editor::Editor;
|
||||
use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
|
||||
use gpui::{actions, prelude::*, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
|
||||
use language::language_settings;
|
||||
use std::{any::TypeId, time::Duration};
|
||||
use std::time::Duration;
|
||||
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
|
@ -21,40 +19,6 @@ actions!(
|
|||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
|
||||
workspace.register_action(|workspace, _: &RateCompletions, window, cx| {
|
||||
if cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>() {
|
||||
RateCompletionModal::toggle(workspace, window, cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
feature_gate_predict_edits_rating_actions(cx);
|
||||
}
|
||||
|
||||
fn feature_gate_predict_edits_rating_actions(cx: &mut App) {
|
||||
let rate_completion_action_types = [TypeId::of::<RateCompletions>()];
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&rate_completion_action_types);
|
||||
});
|
||||
|
||||
cx.observe_flag::<PredictEditsRateCompletionsFeatureFlag, _>(move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_action_types(rate_completion_action_types.iter());
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&rate_completion_action_types);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct RateCompletionModal {
|
||||
zeta: Entity<Zeta>,
|
||||
active_completion: Option<ActiveCompletion>,
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
mod completion_diff_element;
|
||||
mod persistence;
|
||||
mod init;
|
||||
mod license_detection;
|
||||
mod onboarding_banner;
|
||||
mod onboarding_modal;
|
||||
mod rate_completion_modal;
|
||||
|
||||
pub(crate) use completion_diff_element::*;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
pub use init::*;
|
||||
use inline_completion::DataCollectionState;
|
||||
pub use license_detection::is_license_eligible_for_data_collection;
|
||||
pub use onboarding_banner::*;
|
||||
pub use rate_completion_modal::*;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use arrayvec::ArrayVec;
|
||||
use client::{Client, UserStore};
|
||||
use collections::hash_map::Entry;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{
|
||||
actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::{
|
||||
|
@ -24,33 +28,32 @@ use language::{
|
|||
OffsetRangeExt, Point, ToOffset, ToPoint,
|
||||
};
|
||||
use language_models::LlmApiToken;
|
||||
use postage::watch;
|
||||
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
|
||||
use settings::WorktreeId;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp, env,
|
||||
cmp,
|
||||
fmt::Write,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::InlineCompletionRating;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
use workspace::{
|
||||
notifications::{simple_message_notification::MessageNotification, NotificationId},
|
||||
Workspace,
|
||||
};
|
||||
use worktree::Worktree;
|
||||
|
||||
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
|
||||
const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>";
|
||||
const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>";
|
||||
const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>";
|
||||
const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY: &'static str =
|
||||
"zed_predict_data_collection_never_ask_again";
|
||||
const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice";
|
||||
|
||||
// TODO(mgsloan): more systematic way to choose or tune these fairly arbitrary constants?
|
||||
|
||||
|
@ -206,11 +209,12 @@ pub struct Zeta {
|
|||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||
shown_completions: VecDeque<InlineCompletion>,
|
||||
rated_completions: HashSet<InlineCompletionId>,
|
||||
data_collection_preferences: DataCollectionPreferences,
|
||||
data_collection_choice: Entity<DataCollectionChoice>,
|
||||
llm_token: LlmApiToken,
|
||||
_llm_token_subscription: Subscription,
|
||||
tos_accepted: bool, // Terms of service accepted
|
||||
_user_store_subscription: Subscription,
|
||||
license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
|
||||
}
|
||||
|
||||
impl Zeta {
|
||||
|
@ -219,15 +223,28 @@ impl Zeta {
|
|||
}
|
||||
|
||||
pub fn register(
|
||||
worktree: Option<Entity<Worktree>>,
|
||||
client: Arc<Client>,
|
||||
user_store: Entity<UserStore>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
Self::global(cx).unwrap_or_else(|| {
|
||||
let this = Self::global(cx).unwrap_or_else(|| {
|
||||
let model = cx.new(|cx| Self::new(client, user_store, cx));
|
||||
cx.set_global(ZetaGlobal(model.clone()));
|
||||
model
|
||||
})
|
||||
});
|
||||
|
||||
this.update(cx, move |this, cx| {
|
||||
if let Some(worktree) = worktree {
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
this.license_detection_watchers
|
||||
.entry(worktree.id())
|
||||
.or_insert_with(|| Rc::new(LicenseDetectionWatcher::new(worktree, cx)));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn clear_history(&mut self) {
|
||||
|
@ -236,13 +253,17 @@ impl Zeta {
|
|||
|
||||
fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
|
||||
let refresh_llm_token_listener = language_models::RefreshLlmTokenListener::global(cx);
|
||||
|
||||
let data_collection_choice = Self::load_data_collection_choices();
|
||||
let data_collection_choice = cx.new(|_| data_collection_choice);
|
||||
|
||||
Self {
|
||||
client,
|
||||
events: VecDeque::new(),
|
||||
shown_completions: VecDeque::new(),
|
||||
rated_completions: HashSet::default(),
|
||||
registered_buffers: HashMap::default(),
|
||||
data_collection_preferences: Self::load_data_collection_preferences(cx),
|
||||
data_collection_choice,
|
||||
llm_token: LlmApiToken::default(),
|
||||
_llm_token_subscription: cx.subscribe(
|
||||
&refresh_llm_token_listener,
|
||||
|
@ -271,6 +292,7 @@ impl Zeta {
|
|||
_ => {}
|
||||
}
|
||||
}),
|
||||
license_detection_watchers: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,7 +364,7 @@ impl Zeta {
|
|||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
cursor: language::Anchor,
|
||||
can_collect_data: bool,
|
||||
data_collection_permission: bool,
|
||||
cx: &mut Context<Self>,
|
||||
perform_predict_edits: F,
|
||||
) -> Task<Result<Option<InlineCompletion>>>
|
||||
|
@ -407,7 +429,7 @@ impl Zeta {
|
|||
input_events: input_events.clone(),
|
||||
input_excerpt: input_excerpt.clone(),
|
||||
outline: Some(input_outline.clone()),
|
||||
can_collect_data,
|
||||
data_collection_permission,
|
||||
};
|
||||
|
||||
let response = perform_predict_edits(client, llm_token, is_staff, body).await?;
|
||||
|
@ -587,13 +609,13 @@ and then another
|
|||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
can_collect_data: bool,
|
||||
data_collection_permission: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<InlineCompletion>>> {
|
||||
self.request_completion_impl(
|
||||
buffer,
|
||||
position,
|
||||
can_collect_data,
|
||||
data_collection_permission,
|
||||
cx,
|
||||
Self::perform_predict_edits,
|
||||
)
|
||||
|
@ -903,84 +925,55 @@ and then another
|
|||
new_snapshot
|
||||
}
|
||||
|
||||
/// Creates a `Entity<DataCollectionChoice>` for each unique worktree abs path it sees.
|
||||
pub fn data_collection_choice_at(
|
||||
&mut self,
|
||||
worktree_abs_path: PathBuf,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<DataCollectionChoice> {
|
||||
match self
|
||||
.data_collection_preferences
|
||||
.per_worktree
|
||||
.entry(worktree_abs_path)
|
||||
{
|
||||
Entry::Vacant(entry) => {
|
||||
let choice = cx.new(|_| DataCollectionChoice::NotAnswered);
|
||||
entry.insert(choice.clone());
|
||||
choice
|
||||
fn load_data_collection_choices() -> DataCollectionChoice {
|
||||
let choice = KEY_VALUE_STORE
|
||||
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
|
||||
.log_err()
|
||||
.flatten();
|
||||
|
||||
match choice.as_deref() {
|
||||
Some("true") => DataCollectionChoice::Enabled,
|
||||
Some("false") => DataCollectionChoice::Disabled,
|
||||
Some(_) => {
|
||||
log::error!("unknown value in '{ZED_PREDICT_DATA_COLLECTION_CHOICE}'");
|
||||
DataCollectionChoice::NotAnswered
|
||||
}
|
||||
Entry::Occupied(entry) => entry.get().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_never_ask_again_for_data_collection(&mut self, cx: &mut Context<Self>) {
|
||||
self.data_collection_preferences.never_ask_again = true;
|
||||
|
||||
// persist choice
|
||||
db::write_and_log(cx, move || {
|
||||
KEY_VALUE_STORE.write_kvp(
|
||||
ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY.into(),
|
||||
"true".to_string(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn load_data_collection_preferences(cx: &mut Context<Self>) -> DataCollectionPreferences {
|
||||
if env::var("ZED_PREDICT_CLEAR_DATA_COLLECTION_PREFERENCES").is_ok() {
|
||||
db::write_and_log(cx, move || async move {
|
||||
KEY_VALUE_STORE
|
||||
.delete_kvp(ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY.into())
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
persistence::DB.clear_all_zeta_preferences().await
|
||||
});
|
||||
return DataCollectionPreferences::default();
|
||||
}
|
||||
|
||||
let never_ask_again = KEY_VALUE_STORE
|
||||
.read_kvp(ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY)
|
||||
.log_err()
|
||||
.flatten()
|
||||
.map(|value| value == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
let preferences_per_worktree = persistence::DB
|
||||
.get_all_data_collection_preferences()
|
||||
.log_err()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(path, choice)| {
|
||||
let choice = cx.new(|_| DataCollectionChoice::from(choice));
|
||||
(path, choice)
|
||||
})
|
||||
.collect();
|
||||
|
||||
DataCollectionPreferences {
|
||||
never_ask_again,
|
||||
per_worktree: preferences_per_worktree,
|
||||
None => DataCollectionChoice::NotAnswered,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct DataCollectionPreferences {
|
||||
/// Set when a user clicks on "Never Ask Again", can never be unset.
|
||||
never_ask_again: bool,
|
||||
/// The choices for each worktree.
|
||||
///
|
||||
/// This is filled when loading from database, or when querying if no matching path is found.
|
||||
per_worktree: HashMap<PathBuf, Entity<DataCollectionChoice>>,
|
||||
struct LicenseDetectionWatcher {
|
||||
is_open_source_rx: watch::Receiver<bool>,
|
||||
_is_open_source_task: Task<()>,
|
||||
}
|
||||
|
||||
impl LicenseDetectionWatcher {
|
||||
pub fn new(worktree: &Worktree, cx: &mut Context<Worktree>) -> Self {
|
||||
let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::<bool>(false);
|
||||
|
||||
let loaded_file_fut = worktree.load_file(Path::new("LICENSE"), false, cx);
|
||||
|
||||
Self {
|
||||
is_open_source_rx,
|
||||
_is_open_source_task: cx.spawn(|_, _| async move {
|
||||
// TODO: Don't display error if file not found
|
||||
let Some(loaded_file) = loaded_file_fut.await.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let is_loaded_file_open_source_thing: bool =
|
||||
is_license_eligible_for_data_collection(&loaded_file.text);
|
||||
|
||||
*is_open_source_tx.borrow_mut() = is_loaded_file_open_source_thing;
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Answers false until we find out it's open source
|
||||
pub fn is_open_source(&self) -> bool {
|
||||
*self.is_open_source_rx.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
|
||||
|
@ -1308,7 +1301,7 @@ impl DataCollectionChoice {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn toggle(self) -> DataCollectionChoice {
|
||||
pub fn toggle(&self) -> DataCollectionChoice {
|
||||
match self {
|
||||
Self::Enabled => Self::Disabled,
|
||||
Self::Disabled => Self::Enabled,
|
||||
|
@ -1326,87 +1319,93 @@ impl From<bool> for DataCollectionChoice {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ZetaInlineCompletionProvider {
|
||||
zeta: Entity<Zeta>,
|
||||
pending_completions: ArrayVec<PendingCompletion, 2>,
|
||||
next_pending_completion_id: usize,
|
||||
current_completion: Option<CurrentInlineCompletion>,
|
||||
data_collection: Option<ProviderDataCollection>,
|
||||
}
|
||||
|
||||
pub struct ProviderDataCollection {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
worktree_root_path: PathBuf,
|
||||
choice: Entity<DataCollectionChoice>,
|
||||
/// When set to None, data collection is not possible in the provider buffer
|
||||
choice: Option<Entity<DataCollectionChoice>>,
|
||||
license_detection_watcher: Option<Rc<LicenseDetectionWatcher>>,
|
||||
}
|
||||
|
||||
impl ProviderDataCollection {
|
||||
pub fn new(
|
||||
zeta: Entity<Zeta>,
|
||||
workspace: Option<Entity<Workspace>>,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
cx: &mut App,
|
||||
) -> Option<ProviderDataCollection> {
|
||||
let workspace = workspace?;
|
||||
|
||||
let worktree_root_path = buffer?.update(cx, |buffer, cx| {
|
||||
let file = buffer.file()?;
|
||||
pub fn new(zeta: Entity<Zeta>, buffer: Option<Entity<Buffer>>, cx: &mut App) -> Self {
|
||||
let choice_and_watcher = buffer.and_then(|buffer| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
|
||||
if !file.is_local() || file.is_private() {
|
||||
return None;
|
||||
}
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Some(
|
||||
workspace
|
||||
.absolute_path_of_worktree(file.worktree_id(cx), cx)?
|
||||
.to_path_buf(),
|
||||
let zeta = zeta.read(cx);
|
||||
let choice = zeta.data_collection_choice.clone();
|
||||
|
||||
// Unwrap safety: there should be a watcher for each worktree
|
||||
let license_detection_watcher = zeta
|
||||
.license_detection_watchers
|
||||
.get(&file.worktree_id(cx))
|
||||
.cloned()?;
|
||||
|
||||
Some((choice, license_detection_watcher))
|
||||
});
|
||||
|
||||
if let Some((choice, watcher)) = choice_and_watcher {
|
||||
ProviderDataCollection {
|
||||
choice: Some(choice),
|
||||
license_detection_watcher: Some(watcher),
|
||||
}
|
||||
} else {
|
||||
ProviderDataCollection {
|
||||
choice: None,
|
||||
license_detection_watcher: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data_collection_permission(&self, cx: &App) -> bool {
|
||||
self.choice
|
||||
.as_ref()
|
||||
.is_some_and(|choice| choice.read(cx).is_enabled())
|
||||
&& self
|
||||
.license_detection_watcher
|
||||
.as_ref()
|
||||
.is_some_and(|watcher| watcher.is_open_source())
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self, cx: &mut App) {
|
||||
if let Some(choice) = self.choice.as_mut() {
|
||||
let new_choice = choice.update(cx, |choice, _cx| {
|
||||
let new_choice = choice.toggle();
|
||||
*choice = new_choice;
|
||||
new_choice
|
||||
});
|
||||
|
||||
db::write_and_log(cx, move || {
|
||||
KEY_VALUE_STORE.write_kvp(
|
||||
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
|
||||
new_choice.is_enabled().to_string(),
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
let choice = zeta.update(cx, |zeta, cx| {
|
||||
zeta.data_collection_choice_at(worktree_root_path.clone(), cx)
|
||||
});
|
||||
|
||||
Some(ProviderDataCollection {
|
||||
workspace: workspace.downgrade(),
|
||||
worktree_root_path,
|
||||
choice,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_choice(&mut self, choice: DataCollectionChoice, cx: &mut App) {
|
||||
self.choice.update(cx, |this, _| *this = choice);
|
||||
|
||||
let worktree_root_path = self.worktree_root_path.clone();
|
||||
|
||||
db::write_and_log(cx, move || {
|
||||
persistence::DB.save_data_collection_choice(worktree_root_path, choice.is_enabled())
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_choice(&mut self, cx: &mut App) {
|
||||
self.set_choice(self.choice.read(cx).toggle(), cx);
|
||||
}
|
||||
pub struct ZetaInlineCompletionProvider {
|
||||
zeta: Entity<Zeta>,
|
||||
pending_completions: ArrayVec<PendingCompletion, 2>,
|
||||
next_pending_completion_id: usize,
|
||||
current_completion: Option<CurrentInlineCompletion>,
|
||||
/// None if this is entirely disabled for this provider
|
||||
provider_data_collection: ProviderDataCollection,
|
||||
}
|
||||
|
||||
impl ZetaInlineCompletionProvider {
|
||||
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(8);
|
||||
|
||||
pub fn new(zeta: Entity<Zeta>, data_collection: Option<ProviderDataCollection>) -> Self {
|
||||
pub fn new(zeta: Entity<Zeta>, provider_data_collection: ProviderDataCollection) -> Self {
|
||||
Self {
|
||||
zeta,
|
||||
pending_completions: ArrayVec::new(),
|
||||
next_pending_completion_id: 0,
|
||||
current_completion: None,
|
||||
data_collection,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_data_collection_choice(&mut self, choice: DataCollectionChoice, cx: &mut App) {
|
||||
if let Some(data_collection) = self.data_collection.as_mut() {
|
||||
data_collection.set_choice(choice, cx);
|
||||
provider_data_collection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1433,11 +1432,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
|||
}
|
||||
|
||||
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
|
||||
let Some(data_collection) = self.data_collection.as_ref() else {
|
||||
return DataCollectionState::Unknown;
|
||||
};
|
||||
|
||||
if data_collection.choice.read(cx).is_enabled() {
|
||||
if self.provider_data_collection.data_collection_permission(cx) {
|
||||
DataCollectionState::Enabled
|
||||
} else {
|
||||
DataCollectionState::Disabled
|
||||
|
@ -1445,9 +1440,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
|||
}
|
||||
|
||||
fn toggle_data_collection(&mut self, cx: &mut App) {
|
||||
if let Some(data_collection) = self.data_collection.as_mut() {
|
||||
data_collection.toggle_choice(cx);
|
||||
}
|
||||
self.provider_data_collection.toggle(cx);
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
|
@ -1495,12 +1488,8 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
|||
|
||||
let pending_completion_id = self.next_pending_completion_id;
|
||||
self.next_pending_completion_id += 1;
|
||||
let can_collect_data = self
|
||||
.data_collection
|
||||
.as_ref()
|
||||
.map_or(false, |data_collection| {
|
||||
data_collection.choice.read(cx).is_enabled()
|
||||
});
|
||||
let data_collection_permission =
|
||||
self.provider_data_collection.data_collection_permission(cx);
|
||||
|
||||
let task = cx.spawn(|this, mut cx| async move {
|
||||
if debounce {
|
||||
|
@ -1509,7 +1498,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
|||
|
||||
let completion_request = this.update(&mut cx, |this, cx| {
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.request_completion(&buffer, position, can_collect_data, cx)
|
||||
zeta.request_completion(&buffer, position, data_collection_permission, cx)
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -1596,79 +1585,8 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
|||
// Right now we don't support cycling.
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
self.pending_completions.clear();
|
||||
|
||||
let Some(data_collection) = self.data_collection.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if data_collection.choice.read(cx).is_answered()
|
||||
|| self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.data_collection_preferences
|
||||
.never_ask_again
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
struct ZetaDataCollectionNotification;
|
||||
let notification_id = NotificationId::unique::<ZetaDataCollectionNotification>();
|
||||
|
||||
const DATA_COLLECTION_INFO_URL: &str = "https://zed.dev/terms-of-service"; // TODO: Replace for a link that's dedicated to Edit Predictions data collection
|
||||
|
||||
let this = cx.entity();
|
||||
data_collection
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_notification(notification_id, cx, |cx| {
|
||||
let zeta = self.zeta.clone();
|
||||
|
||||
cx.new(move |_cx| {
|
||||
let message =
|
||||
"To allow Zed to suggest better edits, turn on data collection. You \
|
||||
can turn off at any time via the status bar menu.";
|
||||
MessageNotification::new(message)
|
||||
.with_title("Per-Project Data Collection Program")
|
||||
.show_close_button(false)
|
||||
.with_click_message("Turn On")
|
||||
.on_click({
|
||||
let this = this.clone();
|
||||
move |_window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_data_collection_choice(
|
||||
DataCollectionChoice::Enabled,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_secondary_click_message("Turn Off")
|
||||
.on_secondary_click({
|
||||
move |_window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_data_collection_choice(
|
||||
DataCollectionChoice::Disabled,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_tertiary_click_message("Never Ask Again")
|
||||
.on_tertiary_click({
|
||||
move |_window, cx| {
|
||||
zeta.update(cx, |zeta, cx| {
|
||||
zeta.set_never_ask_again_for_data_collection(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.more_info_message("Learn More")
|
||||
.more_info_url(DATA_COLLECTION_INFO_URL)
|
||||
})
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn discard(&mut self, _cx: &mut Context<Self>) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue