From e1a6d9a4859747daa75d2e0d9d959510a294da59 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 5 Feb 2025 18:09:19 +0100 Subject: [PATCH] edit prediction: Improve UX around `disabled_globs` and `show_inline_completions` (#24207) Release Notes: - N/A --------- Co-authored-by: Danilo Co-authored-by: Danilo Leal --- Cargo.lock | 1 + assets/icons/zed_predict_disabled.svg | 6 + .../src/copilot_completion_provider.rs | 21 +- crates/editor/src/editor.rs | 143 +++-- crates/inline_completion_button/Cargo.toml | 5 +- .../src/inline_completion_button.rs | 250 +++++---- crates/language/src/language_settings.rs | 13 +- .../src/supermaven_completion_provider.rs | 14 +- crates/ui/src/components/context_menu.rs | 491 +++++++++++------- crates/ui/src/components/icon.rs | 1 + crates/vim/src/vim.rs | 2 +- crates/zed/src/zed/quick_action_bar.rs | 35 +- crates/zeta/src/zeta.rs | 15 +- 13 files changed, 578 insertions(+), 419 deletions(-) create mode 100644 assets/icons/zed_predict_disabled.svg diff --git a/Cargo.lock b/Cargo.lock index 7b9fed4f37606949e7d48b0df6e7af5e9de3fecb..42afceedd4b1b3d30c9ef16219f4ea531def35e1 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 0000000000000000000000000000000000000000..d10c4d560a88c718075a5c5dca6abc32daee2ae1 --- /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 9c25e295aa91f75b9186a31519a877402a82e4f9..f953e5a1100371c6990e71e1208bb6e33b15d8bd 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 1ecd630dd65eaa97790c24af98a0806dd8deabd8..dab3ef5d7187805c8ce783c40a40c0c68ac69392 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 e8c51efcaf3405762c5591c712f90928f7525cf7..973e7d327301d17a1830b816e9623ef0eab89083 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 a2b72ed1c2ebc9bea728cd9d2dd8d998c793c512..447141864688bf5757737e450c778fa508181ee8 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); + + 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" + }; - return div().child( - icon_button() - .tooltip(move |window, cx| { + 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.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(edits.iter().map(|e| e.0.clone())); + selections.select_ranges(vec![range]); }); - - // 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); - } } })?; @@ -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 55d284fedb2e15af20c071c0a2768dc6981e858d..ac57e566f4c36aabf505101843ffa35e4342c304 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 01e52b2f8420fea8ac956f28e7879ad0572cbadf..f80551a3f39d3f3a417bded1f4affa1bce46253b 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 765c216ccd0c797e6f05b2f30c0130edf9e41a17..db9632d4ff31e36195c5216f0820d40c512ae47d 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() + let aside = self + .documentation_aside + .as_ref() + .map(|(_, callback)| callback.clone()); + + 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( - 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() + 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), + ); + } } - 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() - }; - - 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), - ) + 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 { + icon_color.unwrap_or(Color::Default) + }; + let label_color = if *disabled { + Color::Muted } else { - v_flex() - .flex_none() - .size(IconSize::default().rems()) + 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_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 c1aea34371067388d474e42036485e99f99eba45..a3e2c1897af3ebccab530122bc649e1cbf08839b 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 1e47a08d2a458b2789c4d4253bde6858f0b4d91b..e331260faa22db5c87eb4c084cd2fd528cf2e58b 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 bd498a126d2b60823284bcdb01273912a704c83d..96e839d523b506a8f0fe5d3fc7ca2498512c3f2e 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 6e68a957c9bd92cccb932ca3b69b97ff5a207d08..7ef46959008e14065cfe27a5302151630c2ca322 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 {