diff --git a/Cargo.lock b/Cargo.lock index 7b9fed4f37..42afceedd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6384,6 +6384,7 @@ dependencies = [ "lsp", "paths", "project", + "regex", "serde_json", "settings", "supermaven", diff --git a/assets/icons/zed_predict_disabled.svg b/assets/icons/zed_predict_disabled.svg new file mode 100644 index 0000000000..d10c4d560a --- /dev/null +++ b/assets/icons/zed_predict_disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 9c25e295aa..f953e5a110 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -2,10 +2,7 @@ use crate::{Completion, Copilot}; use anyhow::Result; use gpui::{App, Context, Entity, EntityId, Task}; use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider}; -use language::{ - language_settings::{all_language_settings, AllLanguageSettings}, - Buffer, OffsetRangeExt, ToOffset, -}; +use language::{language_settings::AllLanguageSettings, Buffer, OffsetRangeExt, ToOffset}; use settings::Settings; use std::{path::Path, time::Duration}; @@ -73,19 +70,11 @@ impl InlineCompletionProvider for CopilotCompletionProvider { fn is_enabled( &self, - buffer: &Entity, - cursor_position: language::Anchor, + _buffer: &Entity, + _cursor_position: language::Anchor, cx: &App, ) -> bool { - if !self.copilot.read(cx).status().is_authorized() { - return false; - } - - let buffer = buffer.read(cx); - let file = buffer.file(); - let language = buffer.language_at(cursor_position); - let settings = all_language_settings(file, cx); - settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) + self.copilot.read(cx).status().is_authorized() } fn refresh( @@ -205,7 +194,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { fn discard(&mut self, cx: &mut Context) { let settings = AllLanguageSettings::get_global(cx); - let copilot_enabled = settings.inline_completions_enabled(None, None, cx); + let copilot_enabled = settings.show_inline_completions(None, cx); if !copilot_enabled { return; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1ecd630dd6..dab3ef5d71 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -680,7 +680,7 @@ pub struct Editor { stale_inline_completion_in_menu: Option, // enable_inline_completions is a switch that Vim can use to disable // edit predictions based on its mode. - enable_inline_completions: bool, + show_inline_completions: bool, show_inline_completions_override: Option, menu_inline_completions_policy: MenuInlineCompletionsPolicy, inlay_hint_cache: InlayHintCache, @@ -1388,7 +1388,7 @@ impl Editor { next_editor_action_id: EditorActionId::default(), editor_actions: Rc::default(), show_inline_completions_override: None, - enable_inline_completions: true, + show_inline_completions: true, menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, custom_context_menu: None, show_git_blame_gutter: false, @@ -1818,9 +1818,9 @@ impl Editor { self.input_enabled = input_enabled; } - pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut Context) { - self.enable_inline_completions = enabled; - if !self.enable_inline_completions { + pub fn set_show_inline_completions_enabled(&mut self, enabled: bool, cx: &mut Context) { + self.show_inline_completions = enabled; + if !self.show_inline_completions { self.take_active_inline_completion(cx); cx.notify(); } @@ -1871,8 +1871,11 @@ impl Editor { if let Some((buffer, cursor_buffer_position)) = self.buffer.read(cx).text_anchor_for_position(cursor, cx) { - let show_inline_completions = - !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx); + let show_inline_completions = !self.should_show_inline_completions_in_buffer( + &buffer, + cursor_buffer_position, + cx, + ); self.set_show_inline_completions(Some(show_inline_completions), window, cx); } } @@ -1888,42 +1891,6 @@ impl Editor { self.refresh_inline_completion(false, true, window, cx); } - pub fn inline_completions_enabled(&self, cx: &App) -> bool { - let cursor = self.selections.newest_anchor().head(); - if let Some((buffer, buffer_position)) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx) - { - self.should_show_inline_completions(&buffer, buffer_position, cx) - } else { - false - } - } - - fn should_show_inline_completions( - &self, - buffer: &Entity, - buffer_position: language::Anchor, - cx: &App, - ) -> bool { - if !self.snippet_stack.is_empty() { - return false; - } - - if self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) { - return false; - } - - if let Some(provider) = self.inline_completion_provider() { - if let Some(show_inline_completions) = self.show_inline_completions_override { - show_inline_completions - } else { - self.mode == EditorMode::Full && provider.is_enabled(buffer, buffer_position, cx) - } - } else { - false - } - } - fn inline_completions_disabled_in_scope( &self, buffer: &Entity, @@ -4650,9 +4617,18 @@ impl Editor { let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + if !self.inline_completions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { + self.discard_inline_completion(false, cx); + return None; + } + if !user_requested - && (!self.enable_inline_completions - || !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx) + && (!self.show_inline_completions + || !self.should_show_inline_completions_in_buffer( + &buffer, + cursor_buffer_position, + cx, + ) || !self.is_focused(window) || buffer.read(cx).is_empty()) { @@ -4665,6 +4641,77 @@ impl Editor { Some(()) } + pub fn should_show_inline_completions(&self, cx: &App) -> bool { + let cursor = self.selections.newest_anchor().head(); + if let Some((buffer, cursor_position)) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx) + { + self.should_show_inline_completions_in_buffer(&buffer, cursor_position, cx) + } else { + false + } + } + + fn should_show_inline_completions_in_buffer( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> bool { + if !self.snippet_stack.is_empty() { + return false; + } + + if self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) { + return false; + } + + if let Some(show_inline_completions) = self.show_inline_completions_override { + show_inline_completions + } else { + let buffer = buffer.read(cx); + self.mode == EditorMode::Full + && language_settings( + buffer.language_at(buffer_position).map(|l| l.name()), + buffer.file(), + cx, + ) + .show_inline_completions + } + } + + pub fn inline_completions_enabled(&self, cx: &App) -> bool { + let cursor = self.selections.newest_anchor().head(); + if let Some((buffer, cursor_position)) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx) + { + self.inline_completions_enabled_in_buffer(&buffer, cursor_position, cx) + } else { + false + } + } + + fn inline_completions_enabled_in_buffer( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> bool { + maybe!({ + let provider = self.inline_completion_provider()?; + if !provider.is_enabled(&buffer, buffer_position, cx) { + return Some(false); + } + let buffer = buffer.read(cx); + let Some(file) = buffer.file() else { + return Some(true); + }; + let settings = all_language_settings(Some(file), cx); + Some(settings.inline_completions_enabled_for_path(file.path())) + }) + .unwrap_or(false) + } + fn cycle_inline_completion( &mut self, direction: Direction, @@ -4675,8 +4722,8 @@ impl Editor { let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if !self.enable_inline_completions - || !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx) + if !self.show_inline_completions + || !self.should_show_inline_completions_in_buffer(&buffer, cursor_buffer_position, cx) { return None; } @@ -5014,7 +5061,7 @@ impl Editor { || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); if completions_menu_has_precedence || !offset_selection.is_empty() - || !self.enable_inline_completions + || !self.show_inline_completions || self .active_inline_completion .as_ref() diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index e8c51efcaf..973e7d3273 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] anyhow.workspace = true +client.workspace = true copilot.workspace = true editor.workspace = true feature_flags.workspace = true @@ -22,14 +23,14 @@ gpui.workspace = true inline_completion.workspace = true language.workspace = true paths.workspace = true +regex.workspace = true settings.workspace = true supermaven.workspace = true +telemetry.workspace = true ui.workspace = true workspace.workspace = true zed_actions.workspace = true zeta.workspace = true -client.workspace = true -telemetry.workspace = true [dev-dependencies] copilot = { workspace = true, features = ["test-support"] } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index a2b72ed1c2..4471418646 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -17,8 +17,12 @@ use language::{ }, File, Language, }; +use regex::Regex; use settings::{update_settings_file, Settings, SettingsStore}; -use std::{path::Path, sync::Arc, time::Duration}; +use std::{ + sync::{Arc, LazyLock}, + time::Duration, +}; use supermaven::{AccountStatus, Supermaven}; use ui::{ prelude::*, Clickable, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, PopoverMenu, @@ -71,9 +75,7 @@ impl Render for InlineCompletionButton { }; let status = copilot.read(cx).status(); - let enabled = self.editor_enabled.unwrap_or_else(|| { - all_language_settings.inline_completions_enabled(None, None, cx) - }); + let enabled = self.editor_enabled.unwrap_or(false); let icon = match status { Status::Error(_) => IconName::CopilotError, @@ -228,25 +230,35 @@ impl Render for InlineCompletionButton { return div(); } - fn icon_button() -> IconButton { - IconButton::new("zed-predict-pending-button", IconName::ZedPredict) - .shape(IconButtonShape::Square) - } + let enabled = self.editor_enabled.unwrap_or(false); + + let zeta_icon = if enabled { + IconName::ZedPredict + } else { + IconName::ZedPredictDisabled + }; let current_user_terms_accepted = self.user_store.read(cx).current_user_has_accepted_terms(); - if !current_user_terms_accepted.unwrap_or(false) { - let signed_in = current_user_terms_accepted.is_some(); - let tooltip_meta = if signed_in { - "Read Terms of Service" - } else { - "Sign in to use" - }; + let icon_button = || { + let base = IconButton::new("zed-predict-pending-button", zeta_icon) + .shape(IconButtonShape::Square); - return div().child( - icon_button() - .tooltip(move |window, cx| { + match ( + current_user_terms_accepted, + self.popover_menu_handle.is_deployed(), + enabled, + ) { + (Some(false) | None, _, _) => { + let signed_in = current_user_terms_accepted.is_some(); + let tooltip_meta = if signed_in { + "Read Terms of Service" + } else { + "Sign in to use" + }; + + base.tooltip(move |window, cx| { Tooltip::with_meta( "Edit Predictions", None, @@ -255,27 +267,37 @@ impl Render for InlineCompletionButton { cx, ) }) - .on_click(cx.listener(move |_, _, window, cx| { - telemetry::event!( - "Pending ToS Clicked", - source = "Edit Prediction Status Button" - ); - window.dispatch_action( - zed_actions::OpenZedPredictOnboarding.boxed_clone(), - cx, - ); - })), - ); - } + .on_click(cx.listener( + move |_, _, window, cx| { + telemetry::event!( + "Pending ToS Clicked", + source = "Edit Prediction Status Button" + ); + window.dispatch_action( + zed_actions::OpenZedPredictOnboarding.boxed_clone(), + cx, + ); + }, + )) + } + (Some(true), true, _) => base, + (Some(true), false, true) => base.tooltip(|window, cx| { + Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx) + }), + (Some(true), false, false) => base.tooltip(|window, cx| { + Tooltip::with_meta( + "Edit Prediction", + Some(&ToggleMenu), + "Disabled For This File", + window, + cx, + ) + }), + } + }; let this = cx.entity().clone(); - 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| { Some(this.update(cx, |this, cx| this.build_zeta_context_menu(window, cx))) @@ -362,15 +384,10 @@ impl InlineCompletionButton { }) } - // Predict Edits at Cursor – alt-tab - // Automatically Predict: - // ✓ PATH - // ✓ Rust - // ✓ All Files pub fn build_language_settings_menu(&self, mut menu: ContextMenu, cx: &mut App) -> ContextMenu { let fs = self.fs.clone(); - menu = menu.header("Predict Edits For:"); + menu = menu.header("Show Predict Edits For"); if let Some(language) = self.language.clone() { let fs = fs.clone(); @@ -381,66 +398,39 @@ impl InlineCompletionButton { menu = menu.toggleable_entry( language.name(), language_enabled, - IconPosition::Start, + IconPosition::End, None, move |_, cx| { - toggle_inline_completions_for_language(language.clone(), fs.clone(), cx) + toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx) }, ); } let settings = AllLanguageSettings::get_global(cx); - if let Some(file) = &self.file { - let path = file.path().clone(); - let path_enabled = settings.inline_completions_enabled_for_path(&path); - - menu = menu.toggleable_entry( - "This File", - path_enabled, - IconPosition::Start, - None, - move |window, cx| { - if let Some(workspace) = window.root().flatten() { - let workspace = workspace.downgrade(); - window - .spawn(cx, |cx| { - configure_disabled_globs( - workspace, - path_enabled.then_some(path.clone()), - cx, - ) - }) - .detach_and_log_err(cx); - } - }, - ); - } - - let globally_enabled = settings.inline_completions_enabled(None, None, cx); + let globally_enabled = settings.show_inline_completions(None, cx); menu = menu.toggleable_entry( "All Files", globally_enabled, - IconPosition::Start, + IconPosition::End, None, move |_, cx| toggle_inline_completions_globally(fs.clone(), cx), ); + menu = menu.separator().header("Privacy Settings"); if let Some(provider) = &self.inline_completion_provider { let data_collection = provider.data_collection_state(cx); - if data_collection.is_supported() { let provider = provider.clone(); let enabled = data_collection.is_enabled(); - menu = menu - .separator() - .header("Help Improve The Model") - .header("Valid Only For OSS Projects"); 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, enabled) + .toggleable(IconPosition::End, data_collection.is_enabled()) + .documentation_aside(|_| { + Label::new("Zed automatically detects if your project is open-source. This setting is only applicable in such cases.").into_any_element() + }) .handler(move |_, cx| { provider.toggle_data_collection(cx); @@ -455,11 +445,42 @@ impl InlineCompletionButton { source = "Edit Prediction Status Menu" ); } - }), - ); + }) + ) } } + menu = menu.item( + ContextMenuEntry::new("Exclude Files") + .documentation_aside(|_| { + Label::new("This item takes you to the settings where you can specify files that will never be captured by any edit prediction model. You can list both specific file extensions and individual file names.").into_any_element() + }) + .handler(move |window, cx| { + if let Some(workspace) = window.root().flatten() { + let workspace = workspace.downgrade(); + window + .spawn(cx, |cx| { + open_disabled_globs_setting_in_editor( + workspace, + cx, + ) + }) + .detach_and_log_err(cx); + } + }), + ); + + if self.file.as_ref().map_or(false, |file| { + !all_language_settings(Some(file), cx).inline_completions_enabled_for_path(file.path()) + }) { + menu = menu.item( + ContextMenuEntry::new("This file is excluded.") + .disabled(true) + .icon(IconName::ZedPredictDisabled) + .icon_size(IconSize::Small), + ); + } + if let Some(editor_focus_handle) = self.editor_focus_handle.clone() { menu = menu .separator() @@ -546,12 +567,11 @@ impl InlineCompletionButton { self.editor_enabled = { let file = file.as_ref(); Some( - file.map(|file| !file.is_private()).unwrap_or(true) - && all_language_settings(file, cx).inline_completions_enabled( - language, - file.map(|file| file.path().as_ref()), - cx, - ), + file.map(|file| { + all_language_settings(Some(file), cx) + .inline_completions_enabled_for_path(file.path()) + }) + .unwrap_or(true), ) }; self.inline_completion_provider = editor.inline_completion_provider(); @@ -616,9 +636,8 @@ impl SupermavenButtonStatus { } } -async fn configure_disabled_globs( +async fn open_disabled_globs_setting_in_editor( workspace: WeakEntity, - path_to_disable: Option>, mut cx: AsyncWindowContext, ) -> Result<()> { let settings_editor = workspace @@ -637,34 +656,34 @@ async fn configure_disabled_globs( let text = item.buffer().read(cx).snapshot(cx).text(); let settings = cx.global::(); - let edits = settings.edits_for_update::(&text, |file| { - let copilot = file.inline_completions.get_or_insert_with(Default::default); - let globs = copilot.disabled_globs.get_or_insert_with(|| { - settings - .get::(None) - .inline_completions - .disabled_globs - .iter() - .map(|glob| glob.glob().to_string()) - .collect() - }); - if let Some(path_to_disable) = &path_to_disable { - globs.push(path_to_disable.to_string_lossy().into_owned()); - } else { - globs.clear(); - } + // Ensure that we always have "inline_completions { "disabled_globs": [] }" + let edits = settings.edits_for_update::(&text, |file| { + file.inline_completions + .get_or_insert_with(Default::default) + .disabled_globs + .get_or_insert_with(Vec::new); }); if !edits.is_empty() { - item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { - selections.select_ranges(edits.iter().map(|e| e.0.clone())); - }); + item.edit(edits.iter().cloned(), cx); + } - // When *enabling* a path, don't actually perform an edit, just select the range. - if path_to_disable.is_some() { - item.edit(edits.iter().cloned(), cx); - } + let text = item.buffer().read(cx).snapshot(cx).text(); + + static DISABLED_GLOBS_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r#""disabled_globs":\s*\[\s*(?P(?:.|\n)*?)\s*\]"#).unwrap() + }); + // Only capture [...] + let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| { + captures + .name("content") + .map(|inner_match| inner_match.start()..inner_match.end()) + }); + if let Some(range) = range { + item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { + selections.select_ranges(vec![range]); + }); } })?; @@ -672,8 +691,7 @@ async fn configure_disabled_globs( } fn toggle_inline_completions_globally(fs: Arc, cx: &mut App) { - let show_inline_completions = - all_language_settings(None, cx).inline_completions_enabled(None, None, cx); + let show_inline_completions = all_language_settings(None, cx).show_inline_completions(None, cx); update_settings_file::(fs, cx, move |file, _| { file.defaults.show_inline_completions = Some(!show_inline_completions) }); @@ -687,9 +705,13 @@ fn set_completion_provider(fs: Arc, cx: &mut App, provider: InlineComple }); } -fn toggle_inline_completions_for_language(language: Arc, fs: Arc, cx: &mut App) { +fn toggle_show_inline_completions_for_language( + language: Arc, + fs: Arc, + cx: &mut App, +) { let show_inline_completions = - all_language_settings(None, cx).inline_completions_enabled(Some(&language), None, cx); + all_language_settings(None, cx).show_inline_completions(Some(&language), cx); update_settings_file::(fs, cx, move |file, _| { file.languages .entry(language.name()) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 55d284fedb..ac57e566f4 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -886,18 +886,7 @@ impl AllLanguageSettings { } /// Returns whether edit predictions are enabled for the given language and path. - pub fn inline_completions_enabled( - &self, - language: Option<&Arc>, - path: Option<&Path>, - cx: &App, - ) -> bool { - if let Some(path) = path { - if !self.inline_completions_enabled_for_path(path) { - return false; - } - } - + pub fn show_inline_completions(&self, language: Option<&Arc>, cx: &App) -> bool { self.language(None, language.map(|l| l.name()).as_ref(), cx) .show_inline_completions } diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 01e52b2f84..f80551a3f3 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -3,7 +3,7 @@ use anyhow::Result; use futures::StreamExt as _; use gpui::{App, Context, Entity, EntityId, Task}; use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider}; -use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot}; +use language::{Anchor, Buffer, BufferSnapshot}; use std::{ ops::{AddAssign, Range}, path::Path, @@ -113,16 +113,8 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { false } - fn is_enabled(&self, buffer: &Entity, cursor_position: Anchor, cx: &App) -> bool { - if !self.supermaven.read(cx).is_enabled() { - return false; - } - - let buffer = buffer.read(cx); - let file = buffer.file(); - let language = buffer.language_at(cursor_position); - let settings = all_language_settings(file, cx); - settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) + fn is_enabled(&self, _buffer: &Entity, _cursor_position: Anchor, cx: &App) -> bool { + self.supermaven.read(cx).is_enabled() } fn is_refreshing(&self) -> bool { diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 765c216ccd..db9632d4ff 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -47,6 +47,7 @@ pub struct ContextMenuEntry { handler: Rc, &mut Window, &mut App)>, action: Option>, disabled: bool, + documentation_aside: Option AnyElement>>, } impl ContextMenuEntry { @@ -61,6 +62,7 @@ impl ContextMenuEntry { handler: Rc::new(|_, _, _| {}), action: None, disabled: false, + documentation_aside: None, } } @@ -108,6 +110,14 @@ impl ContextMenuEntry { self.disabled = disabled; self } + + pub fn documentation_aside( + mut self, + element: impl Fn(&mut App) -> AnyElement + 'static, + ) -> Self { + self.documentation_aside = Some(Rc::new(element)); + self + } } impl From for ContextMenuItem { @@ -125,6 +135,7 @@ pub struct ContextMenu { clicked: bool, _on_blur_subscription: Subscription, keep_open_on_confirm: bool, + documentation_aside: Option<(usize, Rc AnyElement>)>, } impl Focusable for ContextMenu { @@ -161,6 +172,7 @@ impl ContextMenu { clicked: false, _on_blur_subscription, keep_open_on_confirm: false, + documentation_aside: None, }, window, cx, @@ -209,6 +221,7 @@ impl ContextMenu { icon_color: None, action, disabled: false, + documentation_aside: None, })); self } @@ -231,6 +244,7 @@ impl ContextMenu { icon_color: None, action, disabled: false, + documentation_aside: None, })); self } @@ -281,6 +295,7 @@ impl ContextMenu { icon_size: IconSize::Small, icon_color: None, disabled: false, + documentation_aside: None, })); self } @@ -294,7 +309,6 @@ impl ContextMenu { toggle: None, label: label.into(), action: Some(action.boxed_clone()), - handler: Rc::new(move |context, window, cx| { if let Some(context) = &context { window.focus(context); @@ -306,6 +320,7 @@ impl ContextMenu { icon_position: IconPosition::End, icon_color: None, disabled: true, + documentation_aside: None, })); self } @@ -314,7 +329,6 @@ impl ContextMenu { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: None, label: label.into(), - action: Some(action.boxed_clone()), handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), icon: Some(IconName::ArrowUpRight), @@ -322,6 +336,7 @@ impl ContextMenu { icon_position: IconPosition::End, icon_color: None, disabled: false, + documentation_aside: None, })); self } @@ -356,15 +371,16 @@ impl ContextMenu { } fn select_first(&mut self, _: &SelectFirst, _: &mut Window, cx: &mut Context) { - self.selected_index = self.items.iter().position(|item| item.is_selectable()); + if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) { + self.select_index(ix); + } cx.notify(); } pub fn select_last(&mut self) -> Option { for (ix, item) in self.items.iter().enumerate().rev() { if item.is_selectable() { - self.selected_index = Some(ix); - return Some(ix); + return self.select_index(ix); } } None @@ -384,7 +400,7 @@ impl ContextMenu { } else { for (ix, item) in self.items.iter().enumerate().skip(next_index) { if item.is_selectable() { - self.selected_index = Some(ix); + self.select_index(ix); cx.notify(); break; } @@ -402,7 +418,7 @@ impl ContextMenu { } else { for (ix, item) in self.items.iter().enumerate().take(ix).rev() { if item.is_selectable() { - self.selected_index = Some(ix); + self.select_index(ix); cx.notify(); break; } @@ -413,6 +429,20 @@ impl ContextMenu { } } + fn select_index(&mut self, ix: usize) -> Option { + self.documentation_aside = None; + let item = self.items.get(ix)?; + if item.is_selectable() { + self.selected_index = Some(ix); + if let ContextMenuItem::Entry(entry) = item { + if let Some(callback) = &entry.documentation_aside { + self.documentation_aside = Some((ix, callback.clone())); + } + } + } + Some(ix) + } + pub fn on_action_dispatch( &mut self, dispatched: &dyn Action, @@ -436,7 +466,7 @@ impl ContextMenu { false } }) { - self.selected_index = Some(ix); + self.select_index(ix); self.delayed = true; cx.notify(); let action = dispatched.boxed_clone(); @@ -479,198 +509,275 @@ impl Render for ContextMenu { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .flex() - .flex_row() - .child( - v_flex() - .id("context-menu") - .min_w(px(200.)) - .max_h(vh(0.75, window)) - .flex_1() - .overflow_y_scroll() - .track_focus(&self.focus_handle(cx)) - .on_mouse_down_out( - cx.listener(|this, _, window, cx| this.cancel(&menu::Cancel, window, cx)), - ) - .key_context("menu") - .on_action(cx.listener(ContextMenu::select_first)) - .on_action(cx.listener(ContextMenu::handle_select_last)) - .on_action(cx.listener(ContextMenu::select_next)) - .on_action(cx.listener(ContextMenu::select_prev)) - .on_action(cx.listener(ContextMenu::confirm)) - .on_action(cx.listener(ContextMenu::cancel)) - .when(!self.delayed, |mut el| { - for item in self.items.iter() { - if let ContextMenuItem::Entry(ContextMenuEntry { - action: Some(action), - disabled: false, - .. - }) = item - { - el = el.on_boxed_action( - &**action, - cx.listener(ContextMenu::on_action_dispatch), - ); - } - } - el - }) - .child(List::new().children(self.items.iter_mut().enumerate().map( - |(ix, item)| { - match item { - ContextMenuItem::Separator => ListSeparator.into_any_element(), - ContextMenuItem::Header(header) => { - ListSubHeader::new(header.clone()) - .inset(true) - .into_any_element() - } - ContextMenuItem::Label(label) => ListItem::new(ix) - .inset(true) - .disabled(true) - .child(Label::new(label.clone())) - .into_any_element(), - ContextMenuItem::Entry(ContextMenuEntry { - toggle, - label, - handler, - icon, - icon_position, - icon_size, - icon_color, - action, - disabled, - }) => { - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - let icon_color = if *disabled { - Color::Muted - } else { - icon_color.unwrap_or(Color::Default) - }; - let label_color = if *disabled { - Color::Muted - } else { - Color::Default - }; - let label_element = if let Some(icon_name) = icon { - h_flex() - .gap_1p5() - .when(*icon_position == IconPosition::Start, |flex| { - flex.child( - Icon::new(*icon_name) - .size(*icon_size) - .color(icon_color), - ) - }) - .child(Label::new(label.clone()).color(label_color)) - .when(*icon_position == IconPosition::End, |flex| { - flex.child( - Icon::new(*icon_name) - .size(*icon_size) - .color(icon_color), - ) - }) - .into_any_element() - } else { - Label::new(label.clone()) - .color(label_color) - .into_any_element() - }; + let aside = self + .documentation_aside + .as_ref() + .map(|(_, callback)| callback.clone()); - ListItem::new(ix) - .inset(true) - .disabled(*disabled) - .toggle_state(Some(ix) == self.selected_index) - .when_some(*toggle, |list_item, (position, toggled)| { - let contents = if toggled { - v_flex().flex_none().child( - Icon::new(IconName::Check).color(Color::Accent), - ) + h_flex() + .w_full() + .items_start() + .gap_1() + .when_some(aside, |this, aside| { + this.child( + WithRemSize::new(ui_font_size) + .occlude() + .elevation_2(cx) + .p_2() + .max_w_80() + .child(aside(cx)), + ) + }) + .child( + WithRemSize::new(ui_font_size) + .occlude() + .elevation_2(cx) + .flex() + .flex_row() + .child( + v_flex() + .id("context-menu") + .min_w(px(200.)) + .max_h(vh(0.75, window)) + .flex_1() + .overflow_y_scroll() + .track_focus(&self.focus_handle(cx)) + .on_mouse_down_out(cx.listener(|this, _, window, cx| { + this.cancel(&menu::Cancel, window, cx) + })) + .key_context("menu") + .on_action(cx.listener(ContextMenu::select_first)) + .on_action(cx.listener(ContextMenu::handle_select_last)) + .on_action(cx.listener(ContextMenu::select_next)) + .on_action(cx.listener(ContextMenu::select_prev)) + .on_action(cx.listener(ContextMenu::confirm)) + .on_action(cx.listener(ContextMenu::cancel)) + .when(!self.delayed, |mut el| { + for item in self.items.iter() { + if let ContextMenuItem::Entry(ContextMenuEntry { + action: Some(action), + disabled: false, + .. + }) = item + { + el = el.on_boxed_action( + &**action, + cx.listener(ContextMenu::on_action_dispatch), + ); + } + } + el + }) + .child(List::new().children(self.items.iter_mut().enumerate().map( + |(ix, item)| { + match item { + ContextMenuItem::Separator => { + ListSeparator.into_any_element() + } + ContextMenuItem::Header(header) => { + ListSubHeader::new(header.clone()) + .inset(true) + .into_any_element() + } + ContextMenuItem::Label(label) => ListItem::new(ix) + .inset(true) + .disabled(true) + .child(Label::new(label.clone())) + .into_any_element(), + ContextMenuItem::Entry(ContextMenuEntry { + toggle, + label, + handler, + icon, + icon_position, + icon_size, + icon_color, + action, + disabled, + documentation_aside, + }) => { + let handler = handler.clone(); + let menu = cx.entity().downgrade(); + let icon_color = if *disabled { + Color::Muted } else { - v_flex() - .flex_none() - .size(IconSize::default().rems()) + icon_color.unwrap_or(Color::Default) }; - match position { - IconPosition::Start => { - list_item.start_slot(contents) - } - IconPosition::End => list_item.end_slot(contents), - } - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(label_element) - .debug_selector(|| format!("MENU_ITEM-{}", label)) - .children(action.as_ref().and_then(|action| { - self.action_context - .as_ref() - .map(|focus| { - KeyBinding::for_action_in( - &**action, focus, window, + let label_color = if *disabled { + Color::Muted + } else { + Color::Default + }; + let label_element = if let Some(icon_name) = icon { + h_flex() + .gap_1p5() + .when( + *icon_position == IconPosition::Start, + |flex| { + flex.child( + Icon::new(*icon_name) + .size(*icon_size) + .color(icon_color), ) - }) - .unwrap_or_else(|| { - KeyBinding::for_action( - &**action, window, + }, + ) + .child( + Label::new(label.clone()) + .color(label_color), + ) + .when( + *icon_position == IconPosition::End, + |flex| { + flex.child( + Icon::new(*icon_name) + .size(*icon_size) + .color(icon_color), ) - }) - .map(|binding| div().ml_4().child(binding)) - })), - ) - .on_click({ - let context = self.action_context.clone(); - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); + }, + ) + .into_any_element() + } else { + Label::new(label.clone()) + .color(label_color) + .into_any_element() + }; + let documentation_aside_callback = + documentation_aside.clone(); + div() + .id(("context-menu-child", ix)) + .when_some( + documentation_aside_callback, + |this, documentation_aside_callback| { + this.occlude().on_hover(cx.listener( + move |menu, hovered, _, cx| { + if *hovered { + menu.documentation_aside = Some((ix, documentation_aside_callback.clone())); + cx.notify(); + } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) { + menu.documentation_aside = None; + cx.notify(); + } + }, + )) + }, + ) + .child( + ListItem::new(ix) + .inset(true) + .disabled(*disabled) + .toggle_state( + Some(ix) == self.selected_index, + ) + .when_some( + *toggle, + |list_item, (position, toggled)| { + let contents = if toggled { + v_flex().flex_none().child( + Icon::new(IconName::Check) + .color(Color::Accent), + ) + } else { + v_flex().flex_none().size( + IconSize::default().rems(), + ) + }; + match position { + IconPosition::Start => { + list_item + .start_slot(contents) + } + IconPosition::End => { + list_item.end_slot(contents) + } + } + }, + ) + .child( + h_flex() + .w_full() + .justify_between() + .child(label_element) + .debug_selector(|| { + format!("MENU_ITEM-{}", label) + }) + .children( + action.as_ref().and_then( + |action| { + self.action_context + .as_ref() + .map(|focus| { + KeyBinding::for_action_in( + &**action, focus, + window, + ) + }) + .unwrap_or_else(|| { + KeyBinding::for_action( + &**action, window, + ) + }) + .map(|binding| { + div().ml_4().child(binding) + }) + }, + ), + ), + ) + .on_click({ + let context = + self.action_context.clone(); + move |_, window, cx| { + handler( + context.as_ref(), + window, + cx, + ); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + } + }), + ) + .into_any_element() + } + ContextMenuItem::CustomEntry { + entry_render, + handler, + selectable, + } => { + let handler = handler.clone(); + let menu = cx.entity().downgrade(); + let selectable = *selectable; + ListItem::new(ix) + .inset(true) + .toggle_state(if selectable { + Some(ix) == self.selected_index + } else { + false }) - .ok(); - } - }) - .into_any_element() - } - ContextMenuItem::CustomEntry { - entry_render, - handler, - selectable, - } => { - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - let selectable = *selectable; - ListItem::new(ix) - .inset(true) - .toggle_state(if selectable { - Some(ix) == self.selected_index - } else { - false - }) - .selectable(selectable) - .when(selectable, |item| { - item.on_click({ - let context = self.action_context.clone(); - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); + .selectable(selectable) + .when(selectable, |item| { + item.on_click({ + let context = self.action_context.clone(); + move |_, window, cx| { + handler(context.as_ref(), window, cx); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + } }) - .ok(); - } - }) - }) - .child(entry_render(window, cx)) - .into_any_element() - } - } - }, - ))), + }) + .child(entry_render(window, cx)) + .into_any_element() + } + } + }, + ))), + ), ) } } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index c1aea34371..a3e2c1897a 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -323,6 +323,7 @@ pub enum IconName { ZedAssistant2, ZedAssistantFilled, ZedPredict, + ZedPredictDisabled, ZedXCopilot, } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 1e47a08d2a..e331260faa 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1289,7 +1289,7 @@ impl Vim { .map_or(false, |provider| provider.show_completions_in_normal_mode()), _ => false, }; - editor.set_inline_completions_enabled(enable_inline_completions, cx); + editor.set_show_inline_completions_enabled(enable_inline_completions, cx); }); cx.notify() } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index bd498a126d..96e839d523 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -16,8 +16,8 @@ use gpui::{ use search::{buffer_search, BufferSearchBar}; use settings::{Settings, SettingsStore}; use ui::{ - prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize, - PopoverMenu, PopoverMenuHandle, Tooltip, + prelude::*, ButtonStyle, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, IconName, + IconSize, PopoverMenu, PopoverMenuHandle, Tooltip, }; use vim_mode_setting::VimModeSetting; use workspace::{ @@ -94,7 +94,8 @@ impl Render for QuickActionBar { git_blame_inline_enabled, show_git_blame_gutter, auto_signature_help_enabled, - inline_completions_enabled, + show_inline_completions, + inline_completion_enabled, ) = { let editor = editor.read(cx); let selection_menu_enabled = editor.selection_menu_enabled(cx); @@ -103,7 +104,8 @@ impl Render for QuickActionBar { let git_blame_inline_enabled = editor.git_blame_inline_enabled(); let show_git_blame_gutter = editor.show_git_blame_gutter(); let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx); - let inline_completions_enabled = editor.inline_completions_enabled(cx); + let show_inline_completions = editor.should_show_inline_completions(cx); + let inline_completion_enabled = editor.inline_completions_enabled(cx); ( selection_menu_enabled, @@ -112,7 +114,8 @@ impl Render for QuickActionBar { git_blame_inline_enabled, show_git_blame_gutter, auto_signature_help_enabled, - inline_completions_enabled, + show_inline_completions, + inline_completion_enabled, ) }; @@ -294,12 +297,12 @@ impl Render for QuickActionBar { }, ); - menu = menu.toggleable_entry( - "Edit Predictions", - inline_completions_enabled, - IconPosition::Start, - Some(editor::actions::ToggleInlineCompletions.boxed_clone()), - { + let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions") + .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions) + .disabled(!inline_completion_enabled) + .action(Some( + editor::actions::ToggleInlineCompletions.boxed_clone(), + )).handler({ let editor = editor.clone(); move |window, cx| { editor @@ -312,8 +315,14 @@ impl Render for QuickActionBar { }) .ok(); } - }, - ); + }); + if !inline_completion_enabled { + inline_completion_entry = inline_completion_entry.documentation_aside(|_| { + Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() + }); + } + + menu = menu.item(inline_completion_entry); menu = menu.separator(); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 6e68a957c9..7ef4695900 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -25,8 +25,7 @@ use gpui::{ }; use http_client::{HttpClient, Method}; use language::{ - language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview, - OffsetRangeExt, Point, ToOffset, ToPoint, + Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, Point, ToOffset, ToPoint, }; use language_models::LlmApiToken; use postage::watch; @@ -1469,15 +1468,11 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide fn is_enabled( &self, - buffer: &Entity, - cursor_position: language::Anchor, - cx: &App, + _buffer: &Entity, + _cursor_position: language::Anchor, + _cx: &App, ) -> bool { - let buffer = buffer.read(cx); - let file = buffer.file(); - let language = buffer.language_at(cursor_position); - let settings = all_language_settings(file, cx); - settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) + true } fn needs_terms_acceptance(&self, cx: &App) -> bool {