diff --git a/Cargo.lock b/Cargo.lock index 809074fb1bebb63241bd4ac722e29a84190058d1..4a684484c45eb278f03a416458dc1eb71c177bf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10826,6 +10826,7 @@ dependencies = [ "editor", "feature_flags", "gpui", + "language", "menu", "project", "serde_json", @@ -19999,6 +20000,7 @@ dependencies = [ "collab_ui", "collections", "command_palette", + "command_palette_hooks", "component", "copilot", "dap", diff --git a/assets/settings/default.json b/assets/settings/default.json index 985e322cac2a2c4b6b807aeff24caeb68beacf89..0e46115ae327a61e872bb593ba532b11fcb82850 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -25,7 +25,11 @@ // Features that can be globally enabled or disabled "features": { // Which edit prediction provider to use. - "edit_prediction_provider": "zed" + "edit_prediction_provider": "zed", + // A globally enable or disable AI features. + // + // This setting supersedes all other settings related to AI features. + "ai_assistance": true }, // The name of a font to use for rendering text in the editor "buffer_font_family": "Zed Plex Mono", diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index c9c173a68be5191e77690e826378ca52d3db9684..966e18de541517f3fbbbfa820d7223a454b61bdb 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -33,6 +33,7 @@ use gpui::{ App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal, WeakEntity, Window, point, }; +use language::language_settings; use language::{Buffer, Point, Selection, TransactionId}; use language_model::{ ConfigurationError, ConfiguredModel, LanguageModelRegistry, report_assistant_event, @@ -1768,7 +1769,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { _: &mut Window, cx: &mut App, ) -> Task>> { - if !AgentSettings::get_global(cx).enabled { + if !AgentSettings::get_global(cx).enabled || !language_settings::ai_enabled(cx) { return Task::ready(Ok(Vec::new())); } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 9b0abb15379916453eeeeb35860e859a7f721458..f1ce1fed478f361fb99ea136f2c1257dc9e38264 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -54,11 +54,18 @@ pub fn all_language_settings<'a>( AllLanguageSettings::get(location, cx) } +/// Returns whether AI assistance is globally enabled or disabled. +pub fn ai_enabled(cx: &App) -> bool { + all_language_settings(None, cx).ai_assistance +} + /// The settings for all languages. #[derive(Debug, Clone)] pub struct AllLanguageSettings { /// The edit prediction settings. pub edit_predictions: EditPredictionSettings, + /// Whether AI assistance is enabled. + pub ai_assistance: bool, pub defaults: LanguageSettings, languages: HashMap, pub(crate) file_types: FxHashMap, GlobSet>, @@ -646,6 +653,8 @@ pub struct CopilotSettingsContent { pub struct FeaturesContent { /// Determines which edit prediction provider to use. pub edit_prediction_provider: Option, + /// Whether AI assistance is enabled. + pub ai_assistance: Option, } /// Controls the soft-wrapping behavior in the editor. @@ -1122,6 +1131,11 @@ impl AllLanguageSettings { pub fn edit_predictions_mode(&self) -> EditPredictionsMode { self.edit_predictions.mode } + + /// Returns whether AI assistance is enabled. + pub fn is_ai_assistance_enabled(&self) -> bool { + self.ai_assistance + } } fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) { @@ -1247,6 +1261,12 @@ impl settings::Settings for AllLanguageSettings { .map(|settings| settings.enabled_in_text_threads) .unwrap_or(true); + let ai_assistance = default_value + .features + .as_ref() + .and_then(|f| f.ai_assistance) + .unwrap_or(true); + let mut file_types: FxHashMap, GlobSet> = FxHashMap::default(); for (language, patterns) in &default_value.file_types { @@ -1359,6 +1379,7 @@ impl settings::Settings for AllLanguageSettings { copilot: copilot_settings, enabled_in_text_threads, }, + ai_assistance, defaults, languages, file_types, diff --git a/crates/onboarding_ui/Cargo.toml b/crates/onboarding_ui/Cargo.toml index fc903dbaff664bdcafe6a961bdb016cce55af7d1..7bef93e457f5698f23877fd896a8ef42174ee46e 100644 --- a/crates/onboarding_ui/Cargo.toml +++ b/crates/onboarding_ui/Cargo.toml @@ -23,6 +23,7 @@ component.workspace = true db.workspace = true feature_flags.workspace = true gpui.workspace = true +language.workspace = true menu.workspace = true project.workspace = true serde_json.workspace = true diff --git a/crates/onboarding_ui/src/onboarding_ui.rs b/crates/onboarding_ui/src/onboarding_ui.rs index 32818a44671174d1d1c0ac27b7c9ed260d27eb81..4d48ba399f7dd002aeb82981fdb2f0fbea9800bb 100644 --- a/crates/onboarding_ui/src/onboarding_ui.rs +++ b/crates/onboarding_ui/src/onboarding_ui.rs @@ -10,20 +10,22 @@ use client::{Client, TelemetrySettings}; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use gpui::{ - Action, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyBinding, Task, - UpdateGlobal, WeakEntity, actions, prelude::*, svg, transparent_black, + Action, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyBinding, Subscription, + Task, UpdateGlobal, WeakEntity, actions, prelude::*, svg, transparent_black, }; use menu; use persistence::ONBOARDING_DB; +use language::language_settings::{AllLanguageSettings, ai_enabled, all_language_settings}; use project::Project; use serde_json; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, update_settings_file}; use settings_ui::SettingsUiFeatureFlag; +use std::collections::HashSet; use std::sync::Arc; use theme::{Theme, ThemeRegistry, ThemeSettings}; use ui::{ - CheckboxWithLabel, ContentGroup, KeybindingHint, ListItem, Ring, ToggleState, Vector, + CheckboxWithLabel, ContentGroup, FocusOutline, KeybindingHint, ListItem, ToggleState, Vector, VectorName, prelude::*, }; use util::ResultExt; @@ -87,7 +89,7 @@ fn feature_gate_onboarding_ui_actions(cx: &mut App) { .detach(); } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum OnboardingPage { Basics, Editing, @@ -139,7 +141,7 @@ pub struct OnboardingUI { current_page: OnboardingPage, nav_focus: NavigationFocusItem, page_focus: [PageFocusItem; 4], - completed_pages: [bool; 4], + completed_pages: HashSet, focus_area: FocusArea, // Workspace reference for Item trait @@ -147,6 +149,7 @@ pub struct OnboardingUI { workspace_id: Option, client: Arc, welcome_page: Option>, + _settings_subscription: Option, } impl OnboardingUI {} @@ -179,6 +182,8 @@ impl Render for OnboardingUI { .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::toggle_focus)) + .on_action(cx.listener(Self::handle_enable_ai_assistance)) + .on_action(cx.listener(Self::handle_disable_ai_assistance)) .flex() .items_center() .justify_center() @@ -218,31 +223,57 @@ impl Render for OnboardingUI { impl OnboardingUI { pub fn new(workspace: &Workspace, client: Arc, cx: &mut Context) -> Self { + let settings_subscription = cx.observe_global::(|_, cx| { + cx.notify(); + }); + Self { focus_handle: cx.focus_handle(), current_page: OnboardingPage::Basics, nav_focus: NavigationFocusItem::Basics, page_focus: [PageFocusItem(0); 4], - completed_pages: [false; 4], + completed_pages: HashSet::new(), focus_area: FocusArea::Navigation, workspace: workspace.weak_handle(), workspace_id: workspace.database_id(), client, welcome_page: None, + _settings_subscription: Some(settings_subscription), } } fn completed_pages_to_string(&self) -> String { - self.completed_pages - .iter() - .map(|&completed| if completed { '1' } else { '0' }) - .collect() + let mut result = String::new(); + for i in 0..4 { + let page = match i { + 0 => OnboardingPage::Basics, + 1 => OnboardingPage::Editing, + 2 => OnboardingPage::AiSetup, + 3 => OnboardingPage::Welcome, + _ => unreachable!(), + }; + result.push(if self.completed_pages.contains(&page) { + '1' + } else { + '0' + }); + } + result } - fn completed_pages_from_string(s: &str) -> [bool; 4] { - let mut result = [false; 4]; + fn completed_pages_from_string(s: &str) -> HashSet { + let mut result = HashSet::new(); for (i, ch) in s.chars().take(4).enumerate() { - result[i] = ch == '1'; + if ch == '1' { + let page = match i { + 0 => OnboardingPage::Basics, + 1 => OnboardingPage::Editing, + 2 => OnboardingPage::AiSetup, + 3 => OnboardingPage::Welcome, + _ => continue, + }; + result.insert(page); + } } result } @@ -275,7 +306,7 @@ impl OnboardingUI { fn reset(&mut self, _window: &mut gpui::Window, cx: &mut Context) { self.current_page = OnboardingPage::Basics; self.focus_area = FocusArea::Navigation; - self.completed_pages = [false; 4]; + self.completed_pages = HashSet::new(); cx.notify(); } @@ -460,13 +491,7 @@ impl OnboardingUI { _window: &mut gpui::Window, cx: &mut Context, ) { - let index = match page { - OnboardingPage::Basics => 0, - OnboardingPage::Editing => 1, - OnboardingPage::AiSetup => 2, - OnboardingPage::Welcome => 3, - }; - self.completed_pages[index] = true; + self.completed_pages.insert(page); cx.notify(); } @@ -510,6 +535,40 @@ impl OnboardingUI { self.next_page(window, cx); } + fn handle_enable_ai_assistance( + &mut self, + _: &zed_actions::EnableAiAssistance, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(workspace) = self.workspace.upgrade() { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::(fs, cx, move |file, _| { + file.features + .get_or_insert(Default::default()) + .ai_assistance = Some(true); + }); + cx.notify(); + } + } + + fn handle_disable_ai_assistance( + &mut self, + _: &zed_actions::DisableAiAssistance, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(workspace) = self.workspace.upgrade() { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::(fs, cx, move |file, _| { + file.features + .get_or_insert(Default::default()) + .ai_assistance = Some(false); + }); + cx.notify(); + } + } + fn handle_previous_page( &mut self, _: &PreviousPage, @@ -626,7 +685,7 @@ impl OnboardingUI { let area_focused = self.focus_area == FocusArea::Navigation; - Ring::new(corner_radius, item_focused) + FocusOutline::new(corner_radius, item_focused, px(2.)) .active(area_focused && item_focused) .child( h_flex() @@ -1024,6 +1083,10 @@ impl OnboardingUI { let focused_item = self.page_focus[page_index].0; let is_page_focused = self.focus_area == FocusArea::PageContent; + let ai_enabled = ai_enabled(cx); + + let workspace = self.workspace.clone(); + v_flex() .h_full() .w_full() @@ -1035,8 +1098,22 @@ impl OnboardingUI { CheckboxWithLabel::new( "disable_ai", Label::new("Enable AI Features"), - ToggleState::Selected, - |_, _, cx| todo!("implement ai toggle"), + if ai_enabled { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + move |state, _, cx| { + let enabled = state == &ToggleState::Selected; + if let Some(workspace) = workspace.upgrade() { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::(fs, cx, move |file, _| { + file.features + .get_or_insert(Default::default()) + .ai_assistance = Some(enabled); + }); + } + }, ))) .child( CalloutRow::new("We don't use your code to train AI models") @@ -1148,7 +1225,7 @@ impl SerializableItem for OnboardingUI { let completed = OnboardingUI::completed_pages_from_string(&completed_str); (page, completed) } else { - (OnboardingPage::Basics, [false; 4]) + (OnboardingPage::Basics, HashSet::new()) }; cx.update(|window, cx| { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 779e0739bb65bb814fe71b07d85758d3dfc52a26..6305947246a176fa9506b17b7af7dc207d08db2f 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -42,6 +42,7 @@ client.workspace = true collab_ui.workspace = true collections.workspace = true command_palette.workspace = true +command_palette_hooks.workspace = true component.workspace = true copilot.workspace = true dap_adapters.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 10fdcf34a6a1de867668163f15fd3dfe0434f09c..08aa0dae7f8430fcda4813496f473b5fd25a1898 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -16,6 +16,7 @@ use assets::Assets; use breadcrumbs::Breadcrumbs; use client::zed_urls; use collections::VecDeque; +use command_palette_hooks::CommandPaletteFilter; use debugger_ui::debugger_panel::DebugPanel; use editor::ProposedChangesEditorToolbar; use editor::{Editor, MultiBuffer}; @@ -52,6 +53,7 @@ use settings::{ Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, update_settings_file, }; +use std::any::TypeId; use std::path::PathBuf; use std::sync::atomic::{self, AtomicBool}; use std::{borrow::Cow, path::Path, sync::Arc}; @@ -72,7 +74,8 @@ use workspace::{ use workspace::{CloseIntent, CloseWindow, RestoreBanner, with_active_or_new_workspace}; use workspace::{Pane, notifications::DetachAndPromptErr}; use zed_actions::{ - OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettings, OpenZedUrl, Quit, + DisableAiAssistance, EnableAiAssistance, OpenAccountSettings, OpenBrowser, OpenDocs, + OpenServerSettings, OpenSettings, OpenZedUrl, Quit, }; actions!( @@ -215,6 +218,51 @@ pub fn init(cx: &mut App) { ); }); }); + cx.on_action(|_: &EnableAiAssistance, cx| { + with_active_or_new_workspace(cx, |workspace, _, cx| { + let fs = workspace.app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |file, _| { + file.features + .get_or_insert(Default::default()) + .ai_assistance = Some(true); + }, + ); + }); + }); + cx.on_action(|_: &DisableAiAssistance, cx| { + with_active_or_new_workspace(cx, |workspace, _, cx| { + let fs = workspace.app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |file, _| { + file.features + .get_or_insert(Default::default()) + .ai_assistance = Some(false); + }, + ); + }); + }); + + // Filter AI assistance actions based on current state + cx.observe_global::(move |cx| { + let ai_enabled = + language::language_settings::all_language_settings(None, cx).is_ai_assistance_enabled(); + + CommandPaletteFilter::update_global(cx, |filter, _cx| { + if ai_enabled { + filter.hide_action_types(&[TypeId::of::()]); + filter.show_action_types([TypeId::of::()].iter()); + } else { + filter.show_action_types([TypeId::of::()].iter()); + filter.hide_action_types(&[TypeId::of::()]); + } + }); + }) + .detach(); } fn bind_on_window_closed(cx: &mut App) -> Option { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index ffe232ad7bd7e63c6ebfe8af2d1a1c4f37029e85..1daba076b5bb17b152b7fab269eda613acb9ff97 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -50,6 +50,10 @@ actions!( OpenLicenses, /// Opens the telemetry log. OpenTelemetryLog, + /// Enables AI assistance features. + EnableAiAssistance, + /// Disables AI assistance features. + DisableAiAssistance, ] );