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:
co-authored by
Danilo
Danilo Leal
João Marcos
parent
29e559d60c
commit
93f8ccaaee
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use chrono::Utc;
|
||||
use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag};
|
||||
use gpui::Subscription;
|
||||
use language::language_settings::{all_language_settings, InlineCompletionProvider};
|
||||
use settings::SettingsStore;
|
||||
use ui::{prelude::*, ButtonLike, Tooltip};
|
||||
use util::ResultExt;
|
||||
|
||||
/// Prompts the user to try Zed's Edit Prediction feature
|
||||
pub struct ZedPredictBanner {
|
||||
dismissed: bool,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ZedPredictBanner {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
dismissed: get_dismissed(),
|
||||
_subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show(&self, cx: &mut App) -> bool {
|
||||
if !cx.has_flag::<PredictEditsFeatureFlag>() || self.dismissed {
|
||||
return false;
|
||||
}
|
||||
|
||||
let provider = all_language_settings(None, cx).inline_completions.provider;
|
||||
|
||||
match provider {
|
||||
InlineCompletionProvider::None
|
||||
| InlineCompletionProvider::Copilot
|
||||
| InlineCompletionProvider::Supermaven => true,
|
||||
InlineCompletionProvider::Zed => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_settings_changed(&mut self, cx: &mut Context<Self>) {
|
||||
if self.dismissed {
|
||||
return;
|
||||
}
|
||||
|
||||
let provider = all_language_settings(None, cx).inline_completions.provider;
|
||||
|
||||
match provider {
|
||||
InlineCompletionProvider::None
|
||||
| InlineCompletionProvider::Copilot
|
||||
| InlineCompletionProvider::Supermaven => {}
|
||||
InlineCompletionProvider::Zed => {
|
||||
self.dismiss(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
persist_dismissed(cx);
|
||||
self.dismissed = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
const DISMISSED_AT_KEY: &str = "zed_predict_banner_dismissed_at";
|
||||
|
||||
pub(crate) fn get_dismissed() -> bool {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.read_kvp(DISMISSED_AT_KEY)
|
||||
.log_err()
|
||||
.map_or(false, |dismissed| dismissed.is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn persist_dismissed(cx: &mut App) {
|
||||
cx.spawn(|_| {
|
||||
let time = Utc::now().to_rfc3339();
|
||||
db::kvp::KEY_VALUE_STORE.write_kvp(DISMISSED_AT_KEY.into(), time)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
impl Render for ZedPredictBanner {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if !self.should_show(cx) {
|
||||
return div();
|
||||
}
|
||||
|
||||
let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
|
||||
let banner = h_flex()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.child(
|
||||
ButtonLike::new("try-zed-predict")
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::ZedPredict).size(IconSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new("Introducing:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Edit Prediction").size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().border_l_1().border_color(border_color).child(
|
||||
IconButton::new("close", IconName::Close)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Close Announcement Banner",
|
||||
None,
|
||||
"It won't show again for this feature",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
div().pr_2().child(banner)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
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::{
|
||||
ease_in_out, svg, Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
};
|
||||
use language::language_settings::{AllLanguageSettings, InlineCompletionProvider};
|
||||
use settings::{update_settings_file, Settings};
|
||||
use ui::{prelude::*, Checkbox, TintColor, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
|
||||
use worktree::Worktree;
|
||||
|
||||
/// Introduces user to Zed's Edit Prediction feature and terms of service
|
||||
pub struct ZedPredictModal {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
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)]
|
||||
enum SignInStatus {
|
||||
/// Signed out or signed in but not from this modal
|
||||
Idle,
|
||||
/// Authentication triggered from this modal
|
||||
Waiting,
|
||||
/// Signed in after authentication from this modal
|
||||
SignedIn,
|
||||
}
|
||||
|
||||
impl ZedPredictModal {
|
||||
pub fn toggle(
|
||||
workspace: &mut Workspace,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
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,
|
||||
data_collection_expanded: false,
|
||||
data_collection_opted_in: false,
|
||||
worktrees,
|
||||
});
|
||||
}
|
||||
|
||||
fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/terms-of-service");
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/blog/"); // TODO Add the link when live
|
||||
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
|
||||
.update(cx, |this, cx| this.accept_terms_of_service(cx));
|
||||
|
||||
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
|
||||
.get_or_insert(Default::default())
|
||||
.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);
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
|
||||
fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let client = self.client.clone();
|
||||
self.sign_in_status = SignInStatus::Waiting;
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = client.authenticate_and_connect(true, &cx).await;
|
||||
|
||||
let status = match result {
|
||||
Ok(_) => SignInStatus::SignedIn,
|
||||
Err(_) => SignInStatus::Idle,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.sign_in_status = status;
|
||||
cx.notify()
|
||||
})?;
|
||||
|
||||
result
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ZedPredictModal {}
|
||||
|
||||
impl Focusable for ZedPredictModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
.id("zed predict tos")
|
||||
.key_context("ZedPredictModal")
|
||||
.w(px(440.))
|
||||
.p_4()
|
||||
.relative()
|
||||
.gap_2()
|
||||
.overflow_hidden()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_1p5()
|
||||
.absolute()
|
||||
.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(418.))
|
||||
.h(px(128.))
|
||||
.overflow_hidden(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mb_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Introducing Zed AI's")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Edit Prediction").size(HeadlineSize::Large)),
|
||||
)
|
||||
.child({
|
||||
let tab = |n: usize| {
|
||||
let text_color = cx.theme().colors().text;
|
||||
let border_color = cx.theme().colors().text_accent.opacity(0.4);
|
||||
|
||||
h_flex().child(
|
||||
h_flex()
|
||||
.px_4()
|
||||
.py_0p5()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.rounded_md()
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.text_color(text_color)
|
||||
.child("tab")
|
||||
.with_animation(
|
||||
ElementId::Integer(n),
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
move |tab, delta| {
|
||||
let delta = (delta - 0.15 * n as f32) / 0.7;
|
||||
let delta = 1.0 - (0.5 - delta).abs() * 2.;
|
||||
let delta = ease_in_out(delta.clamp(0., 1.));
|
||||
let delta = 0.1 + 0.9 * delta;
|
||||
|
||||
tab.border_color(border_color.opacity(delta))
|
||||
.text_color(text_color.opacity(delta))
|
||||
},
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.pr_4()
|
||||
.child(tab(0).ml_neg_20())
|
||||
.child(tab(1))
|
||||
.child(tab(2).ml_20())
|
||||
}),
|
||||
)
|
||||
.child(h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
));
|
||||
|
||||
let blog_post_button = if cx.is_staff() {
|
||||
Some(
|
||||
Button::new("view-blog", "Read the Blog Post")
|
||||
.full_width()
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(Self::view_blog)),
|
||||
)
|
||||
} else {
|
||||
// TODO: put back when blog post is published
|
||||
None
|
||||
};
|
||||
|
||||
if self.user_store.read(cx).current_user().is_some() {
|
||||
let copy = match self.sign_in_status {
|
||||
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()
|
||||
.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)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.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()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("accept-tos", "Enable Edit Predictions")
|
||||
.disabled(!self.terms_of_service)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::accept_and_enable)),
|
||||
)
|
||||
.children(blog_post_button),
|
||||
)
|
||||
} else {
|
||||
base.child(
|
||||
Label::new("To set Zed as your inline completions provider, please sign in.")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("accept-tos", "Sign in with GitHub")
|
||||
.disabled(self.sign_in_status == SignInStatus::Waiting)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::sign_in)),
|
||||
)
|
||||
.children(blog_post_button),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
+157
-239
@@ -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>) {
|
||||
|
||||
Reference in New Issue
Block a user