edit prediction: Improve UX around disabled_globs and show_inline_completions (#24207)

Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
Bennet Bo Fenner 2025-02-05 18:09:19 +01:00 committed by GitHub
parent 37db1dcd48
commit e1a6d9a485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 580 additions and 421 deletions

1
Cargo.lock generated
View file

@ -6384,6 +6384,7 @@ dependencies = [
"lsp",
"paths",
"project",
"regex",
"serde_json",
"settings",
"supermaven",

View file

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M6.75 9.31247L8.25 10.5576V11.75H1.75V10.0803L4.49751 7.44273L5.65909 8.40693L3.73923 10.25H6.75V9.31247ZM8.25 5.85739V4.25H6.31358L8.25 5.85739ZM1.75 5.16209V7.1H3.25V6.4072L1.75 5.16209Z" fill="black"/>
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M10.9624 9.40853L11.9014 8L10.6241 6.08397L9.37598 6.91603L10.0986 8L9.80184 8.44518L10.9624 9.40853Z" fill="black"/>
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M12.8936 11.0116L14.9014 8L12.6241 4.58397L11.376 5.41603L13.0986 8L11.7331 10.0483L12.8936 11.0116Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1225 13.809C14.0341 13.9146 13.877 13.9289 13.7711 13.8409L1.19311 3.40021C1.08659 3.31178 1.07221 3.15362 1.16104 3.04743L1.87752 2.19101C1.96588 2.0854 2.123 2.07112 2.22895 2.15906L14.8069 12.5998C14.9134 12.6882 14.9278 12.8464 14.839 12.9526L14.1225 13.809Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -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<Buffer>,
cursor_position: language::Anchor,
_buffer: &Entity<Buffer>,
_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<Self>) {
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;

View file

@ -680,7 +680,7 @@ pub struct Editor {
stale_inline_completion_in_menu: Option<InlineCompletionState>,
// 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<bool>,
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>) {
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>) {
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>,
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<Buffer>,
@ -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>,
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>,
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()

View file

@ -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"] }

View file

@ -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<Workspace>,
path_to_disable: Option<Arc<Path>>,
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::<SettingsStore>();
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
let copilot = file.inline_completions.get_or_insert_with(Default::default);
let globs = copilot.disabled_globs.get_or_insert_with(|| {
settings
.get::<AllLanguageSettings>(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::<AllLanguageSettings>(&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<Regex> = LazyLock::new(|| {
Regex::new(r#""disabled_globs":\s*\[\s*(?P<content>(?:.|\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<dyn Fs>, 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::<AllLanguageSettings>(fs, cx, move |file, _| {
file.defaults.show_inline_completions = Some(!show_inline_completions)
});
@ -687,9 +705,13 @@ fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: InlineComple
});
}
fn toggle_inline_completions_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut App) {
fn toggle_show_inline_completions_for_language(
language: Arc<Language>,
fs: Arc<dyn Fs>,
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::<AllLanguageSettings>(fs, cx, move |file, _| {
file.languages
.entry(language.name())

View file

@ -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<Language>>,
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<Language>>, cx: &App) -> bool {
self.language(None, language.map(|l| l.name()).as_ref(), cx)
.show_inline_completions
}

View file

@ -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<Buffer>, 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<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
self.supermaven.read(cx).is_enabled()
}
fn is_refreshing(&self) -> bool {

View file

@ -47,6 +47,7 @@ pub struct ContextMenuEntry {
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
action: Option<Box<dyn Action>>,
disabled: bool,
documentation_aside: Option<Rc<dyn Fn(&mut App) -> 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<ContextMenuEntry> 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<dyn Fn(&mut App) -> 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>) {
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<usize> {
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<usize> {
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<Self>) -> 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()
}
}
},
))),
),
)
}
}

View file

@ -323,6 +323,7 @@ pub enum IconName {
ZedAssistant2,
ZedAssistantFilled,
ZedPredict,
ZedPredictDisabled,
ZedXCopilot,
}

View file

@ -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()
}

View file

@ -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();

View file

@ -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<Buffer>,
cursor_position: language::Anchor,
cx: &App,
_buffer: &Entity<Buffer>,
_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 {