Files
galaxy/app/src/editor/accept_autosuggestion_keybinding_view.rs
T

363 lines
15 KiB
Rust

//! This module contains the code for the editable accept autosuggestion keybinding
//! shown inline in the input.
use galaxy_core::ui::theme::Fill;
use lazy_static::lazy_static;
use pathfinder_geometry::vector::vec2f;
use warpui::elements::{
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Element, Flex, Hoverable, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
ParentOffsetBounds, Radius, Stack, DEFAULT_UI_LINE_HEIGHT_RATIO,
};
use warpui::keymap::Keystroke;
use warpui::platform::Cursor;
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
use warpui::ui_components::keyboard_shortcut::KeyboardShortcut;
use warpui::{AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle};
use super::EditorElement;
use crate::appearance::Appearance;
use crate::editor::ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME;
use crate::menu::{Menu, MenuItemFields};
use crate::settings_view::keybindings::{KeybindingChangedEvent, KeybindingChangedNotifier};
use crate::terminal::input::OPEN_COMPLETIONS_KEYBINDING_NAME;
use crate::ui_components::blended_colors;
use crate::ui_components::icons::Icon;
use crate::util::bindings::{
keybinding_name_to_keystroke, reset_keybinding_to_default, set_custom_keybinding,
};
use crate::workspace::WorkspaceAction;
pub const AUTOSUGGESTION_HINT_MINIMUM_HEIGHT: f32 = 12.;
lazy_static! {
static ref TAB_KEYSTROKE: Keystroke = Keystroke {
key: "tab".to_string(),
..Default::default()
};
static ref CTRL_SPACE_KEYSTROKE: Keystroke = Keystroke {
key: " ".into(),
ctrl: true,
..Default::default()
};
}
pub struct AcceptAutosuggestionKeybinding {
is_menu_open: bool,
select_keybinding_menu: ViewHandle<Menu<AcceptAutosuggestionKeybindingAction>>,
autosuggestion_hint_mouse_handle: MouseStateHandle,
accept_autosuggestion_keybinding: Option<Keystroke>,
}
pub enum AcceptAutosuggestionKeybindingEvent {
SetAcceptAutosuggestionKeybinding {},
OpenSettingsForCustomKeybinding,
}
impl Entity for AcceptAutosuggestionKeybinding {
type Event = AcceptAutosuggestionKeybindingEvent;
}
impl AcceptAutosuggestionKeybinding {
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let accept_autosuggestion_keybinding =
keybinding_name_to_keystroke(ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME, ctx);
let select_keybinding_menu = ctx.add_typed_action_view(|ctx| {
Menu::new().with_border(
Border::all(1.)
.with_border_color(blended_colors::neutral_4(Appearance::as_ref(ctx).theme())),
)
});
let right_arrow = Keystroke::parse("right").expect("can parse keystroke");
let tab = Keystroke::parse("tab").expect("can parse keystroke");
let shift_right_arrow = Keystroke::parse("shift-right").expect("can parse keystroke");
let menu_items: Vec<crate::menu::MenuItem<AcceptAutosuggestionKeybindingAction>> = vec![
MenuItemFields::new(right_arrow.displayed())
.with_on_select_action(
AcceptAutosuggestionKeybindingAction::SetAcceptAutosuggestionKeybinding {
keystroke: right_arrow,
},
)
.into_item(),
MenuItemFields::new(tab.displayed().to_lowercase())
.with_on_select_action(
AcceptAutosuggestionKeybindingAction::SetAcceptAutosuggestionKeybinding {
keystroke: tab,
},
)
.into_item(),
MenuItemFields::new(shift_right_arrow.displayed().to_lowercase())
.with_on_select_action(
AcceptAutosuggestionKeybindingAction::SetAcceptAutosuggestionKeybinding {
keystroke: shift_right_arrow,
},
)
.into_item(),
MenuItemFields::new("Custom...")
.with_on_select_action(
AcceptAutosuggestionKeybindingAction::OpenSettingsForCustomKeybinding,
)
.into_item(),
];
select_keybinding_menu.update(ctx, |menu, ctx| {
menu.set_items(menu_items, ctx);
});
ctx.subscribe_to_view(&select_keybinding_menu, |me, _, event, ctx| {
if let crate::menu::Event::Close { .. } = event {
me.close_menu(ctx);
}
});
// Subscribe to when the accept autosuggestion keybinding changes.
let notifier = KeybindingChangedNotifier::handle(ctx);
ctx.subscribe_to_model(&notifier, |me, _, event, ctx| match event {
KeybindingChangedEvent::BindingChanged {
binding_name,
new_trigger,
} => {
if binding_name == ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME {
me.accept_autosuggestion_keybinding = new_trigger.clone();
me.update_menu_selected_item(ctx);
}
}
});
let mut me = Self {
is_menu_open: false,
select_keybinding_menu,
autosuggestion_hint_mouse_handle: Default::default(),
accept_autosuggestion_keybinding,
};
me.update_menu_selected_item(ctx);
me
}
pub fn close_menu(&mut self, ctx: &mut ViewContext<Self>) {
self.is_menu_open = false;
ctx.notify();
}
/// Updates the menu selected item based on the current accept autosuggestion keybinding.
fn update_menu_selected_item(&mut self, ctx: &mut ViewContext<Self>) {
let accept_autosuggestion_keybinding_displayed = self
.accept_autosuggestion_keybinding
.as_ref()
.map(|keystroke| keystroke.displayed().to_lowercase());
self.select_keybinding_menu.update(ctx, |menu, ctx| {
if let Some(accept_autosuggestion_keybinding_displayed) =
accept_autosuggestion_keybinding_displayed
{
let found =
menu.set_selected_by_name(accept_autosuggestion_keybinding_displayed, ctx);
// If the keybinding is not one of our default options, select the "Custom..." item.
if !found {
menu.set_selected_by_name("Custom...", ctx);
}
} else {
// If the keybinding is not set, we show right arrow which always works.
menu.set_selected_by_action(
&AcceptAutosuggestionKeybindingAction::SetAcceptAutosuggestionKeybinding {
keystroke: Keystroke::parse("right").expect("can parse keystroke"),
},
ctx,
);
}
});
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AcceptAutosuggestionKeybindingAction {
SetAcceptAutosuggestionKeybinding { keystroke: Keystroke },
OpenSettingsForCustomKeybinding,
OpenMenu,
}
impl TypedActionView for AcceptAutosuggestionKeybinding {
type Action = AcceptAutosuggestionKeybindingAction;
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
match action {
AcceptAutosuggestionKeybindingAction::SetAcceptAutosuggestionKeybinding {
keystroke,
} => {
// When set to right arrow, we reset to the default (clears keybinding)
// so that it's still possible to use right arrow to navigate the input when the cursor isn't at the end of the line.
if keystroke.displayed() == "→" {
reset_keybinding_to_default(ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME, ctx);
} else {
// Whatever keybinding is set here will always work regardless of where the cursor is.
set_custom_keybinding(ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME, keystroke, ctx);
}
let open_completions_keystroke =
keybinding_name_to_keystroke(OPEN_COMPLETIONS_KEYBINDING_NAME, ctx);
// If we're setting accept autosuggestion to tab and that conflicts with open completions,
// set open completions to ctrl-space.
if *keystroke == *TAB_KEYSTROKE
&& open_completions_keystroke
.as_ref()
.is_some_and(|keystroke| *keystroke == *TAB_KEYSTROKE)
{
set_custom_keybinding(
OPEN_COMPLETIONS_KEYBINDING_NAME,
&CTRL_SPACE_KEYSTROKE,
ctx,
);
} else if *keystroke != *TAB_KEYSTROKE
&& open_completions_keystroke
.is_some_and(|keystroke| keystroke == *CTRL_SPACE_KEYSTROKE)
{
// If we're setting accept autosuggestion to anything other than tab, switch open completions back to tab if it was set to ctrl-space above.
// Note that we can't tell if the user explicitly set it to ctrl-space vs we did it as a default above.
reset_keybinding_to_default(OPEN_COMPLETIONS_KEYBINDING_NAME, ctx);
}
}
AcceptAutosuggestionKeybindingAction::OpenSettingsForCustomKeybinding => ctx
.dispatch_typed_action(&WorkspaceAction::ConfigureKeybindingSettings {
keybinding_name: Some("Accept Autosuggestion".to_owned()),
}),
AcceptAutosuggestionKeybindingAction::OpenMenu => {
self.is_menu_open = true;
}
};
ctx.notify();
}
}
impl View for AcceptAutosuggestionKeybinding {
fn ui_name() -> &'static str {
"AcceptAutosuggestionKeybinding"
}
fn render(&self, ctx: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(ctx);
// Because the icon's origin is NOT the top of the line but the top of the cursor,
// we should render it with line height ratio no larger than DEFAULT_UI_LINE_HEIGHT_RATIO.
// With larger line height ratios, there wouldn't be enough space in the icon to render the text.
// But we do need to account for smaller line height ratios so the text can be rendered in the smaller space.
let line_height_ratio = appearance
.line_height_ratio()
.min(DEFAULT_UI_LINE_HEIGHT_RATIO);
// We want the keybinding icon to be the same height as the cursor in the input.
let height =
EditorElement::cursor_height(appearance.monospace_font_size(), line_height_ratio)
.max(AUTOSUGGESTION_HINT_MINIMUM_HEIGHT);
let disabled_color = blended_colors::semantic_text_disabled(appearance.theme());
// If there's no keybinding set, right arrow always works.
let keystroke = self
.accept_autosuggestion_keybinding
.clone()
.unwrap_or(Keystroke::parse("right").expect("can parse keystroke"));
let border_width = 1.;
// The stack contains the editable keybinding, tooltip that shows upon mouse hover,
// and menu on click.
let mut stack = Stack::new();
if self.is_menu_open {
stack.add_positioned_overlay_child(
ChildView::new(&self.select_keybinding_menu).finish(),
OffsetPositioning::offset_from_parent(
vec2f(0., -1.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::TopLeft,
ChildAnchor::BottomLeft,
),
);
}
let is_menu_open = self.is_menu_open;
Hoverable::new(self.autosuggestion_hint_mouse_handle.clone(), |state| {
// Colors are inverted when hovered or menu is open.
let (font_color, background_color) = if is_menu_open || state.is_hovered() {
(
appearance.theme().background().into(),
Some(blended_colors::semantic_text_disabled(appearance.theme())),
)
} else {
(
blended_colors::semantic_text_disabled(appearance.theme()),
None,
)
};
let font_size = appearance.monospace_font_size() - 2.;
// We use UI font family for the keyboard shortcut even though it's in the input
// because monospace font family is configurable, and might not handle symbols like
// modifiers and arrows well.
let keystroke = KeyboardShortcut::new(
&keystroke,
UiComponentStyles {
font_family_id: Some(appearance.ui_font_family()),
font_size: Some(font_size),
font_color: Some(font_color),
..Default::default()
},
)
.lowercase_modifier()
.text_only()
.with_line_height_ratio(line_height_ratio)
.build()
.finish();
let height_without_border = height - border_width * 2.;
let chevron_down = Container::new(
ConstrainedBox::new(
Icon::ArrowDropDown
.to_galaxyui_icon(Fill::Solid(font_color))
.finish(),
)
.with_height(height_without_border)
.with_width(height_without_border)
.finish(),
)
.finish();
let mut editable_keystroke = Container::new(
Flex::row()
.with_children([keystroke, chevron_down])
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.finish(),
)
// Padding on the left for the keyboard shortcut.
// The arrow down icon already has its own padding so no need on the right.
.with_padding_left(4.)
.with_border(Border::all(border_width).with_border_color(disabled_color))
.with_corner_radius(CornerRadius::with_all(Radius::Percentage(25.)));
if let Some(background_color) = background_color {
editable_keystroke = editable_keystroke.with_background_color(background_color);
}
let editable_keystroke_element = ConstrainedBox::new(editable_keystroke.finish())
.with_max_height(height)
.finish();
stack.add_child(editable_keystroke_element);
// Add tooltip on hover.
if !is_menu_open && state.is_hovered() {
let tool_tip = appearance
.ui_builder()
.autosuggestion_tool_tip("Change keybinding".into())
.build()
.finish();
stack.add_positioned_overlay_child(
tool_tip,
OffsetPositioning::offset_from_parent(
vec2f(0., -4.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::TopLeft,
ChildAnchor::BottomLeft,
),
);
}
stack.finish()
})
.on_click(|ctx, _, _| {
ctx.dispatch_typed_action(AcceptAutosuggestionKeybindingAction::OpenMenu);
})
.with_cursor(Cursor::PointingHand)
.finish()
}
}