Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions crates/base/src/input/base/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,32 @@ pub trait InputModeKind: sealed::Sealed + Sized + 'static {
impl InputModeKind for InputMode {
const MULTI_LINE: bool = false;

/// A single-line field needs nothing beyond the shared engine. Masking,
/// validation and number stepping live there: together they are ~120 bytes
/// and their access sites sit inside the shared edit path, so separating
/// them would cost more in dispatch than it saves.
type Extras = ();
/// A single-line field needs almost nothing beyond the shared engine.
/// Masking, validation and number stepping live there: together they are
/// ~120 bytes and their access sites sit inside the shared edit path, so
/// separating them would cost more in dispatch than it saves. The one
/// addition is the host-driven completion menu.
type Extras = FieldExtras;

fn hide_context_menu(
state: &mut InputBaseState<Self>,
cx: &mut gpui::Context<InputBaseState<Self>>,
) {
state.hide_context_menu(cx);
}

fn is_context_menu_open(state: &InputBaseState<Self>, _cx: &gpui::App) -> bool {
state.is_context_menu_open()
}

fn handle_context_menu_action(
state: &mut InputBaseState<Self>,
action: Box<dyn gpui::Action>,
window: &mut Window,
cx: &mut gpui::Context<InputBaseState<Self>>,
) -> bool {
state.handle_action_for_context_menu(action, window, cx)
}
}
impl InputModeKind for TextareaMode {
const MULTI_LINE: bool = true;
Expand All @@ -349,6 +370,16 @@ impl InputModeKind for TextareaMode {
// `EditorMode`'s implementation lives with the editor code, next to the
// language features it dispatches to.

/// What a single-line field adds on top of the shared engine: a completion
/// menu whose items the host supplies with
/// [`InputBaseState::present_completion_items`].
#[derive(Default)]
pub struct FieldExtras {
pub(crate) completion: crate::input::CompletionMenuState,
}

impl InputExtras for FieldExtras {}

/// What a code editor adds on top of multi-line text: language features.
pub struct EditorExtras {
pub(crate) lsp: Lsp,
Expand Down
15 changes: 15 additions & 0 deletions crates/base/src/input/base/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,21 @@ impl<M: InputModeKind> InputBaseState<M> {
self.move_vertical(-1, true, window, cx);
}

/// Hands a menu command (next, previous, accept, dismiss) to the open menu.
///
/// These actions are bound only while a menu is open, but a binding can be
/// broader than that, so an unclaimed command bubbles to the host.
pub(super) fn overlay_menu_action<A: gpui::Action + Clone>(
&mut self,
action: &A,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
cx.propagate();
}
}

pub(super) fn down(&mut self, action: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
return;
Expand Down
42 changes: 41 additions & 1 deletion crates/base/src/input/base/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ actions!(
Escape,
ToggleCodeActions,
ShowCompletions,
OverlayMenuNext,
OverlayMenuPrevious,
OverlayMenuAccept,
OverlayMenuDismiss,
Search,
Replace,
GoToDefinition,
Expand All @@ -133,6 +137,12 @@ pub enum InputEvent {

pub(super) const CONTEXT: &str = "Input";

/// Added to the input's key context while a completion or code-action menu is
/// open. The menu bindings below match `Input && menu_open`, so applications
/// rebind them without touching caret movement or editing keys.
pub(super) const MENU_OPEN_CONTEXT: &str = "menu_open";
const MENU_CONTEXT: &str = "Input && menu_open";

pub(crate) fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
Expand Down Expand Up @@ -305,6 +315,13 @@ pub(crate) fn init(cx: &mut App) {
KeyBinding::new("cmd-shift-f", Replace, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-h", Replace, Some(CONTEXT)),
// Registered last so they win over the plain bindings above while a menu
// is open: GPUI breaks ties between equally deep contexts by order.
KeyBinding::new("up", OverlayMenuPrevious, Some(MENU_CONTEXT)),
KeyBinding::new("down", OverlayMenuNext, Some(MENU_CONTEXT)),
KeyBinding::new("enter", OverlayMenuAccept, Some(MENU_CONTEXT)),
KeyBinding::new("shift-enter", OverlayMenuAccept, Some(MENU_CONTEXT)),
KeyBinding::new("escape", OverlayMenuDismiss, Some(MENU_CONTEXT)),
]);
}

Expand Down Expand Up @@ -4621,7 +4638,14 @@ impl<M: InputModeKind> Render for InputBaseState<M> {

let element = div()
.id("input-state")
.key_context(CONTEXT)
.key_context({
let mut context = gpui::KeyContext::new_with_defaults();
context.add(CONTEXT);
if M::is_context_menu_open(self, cx) {
context.add(MENU_OPEN_CONTEXT);
}
context
})
.track_focus(&self.focus_handle)
.when(self.is_editable(), |this| {
this.on_action(window.listener_for(&entity, InputBaseState::backspace))
Expand Down Expand Up @@ -4649,6 +4673,22 @@ impl<M: InputModeKind> Render for InputBaseState<M> {
.on_action(window.listener_for(&entity, InputBaseState::right))
.on_action(window.listener_for(&entity, InputBaseState::select_left))
.on_action(window.listener_for(&entity, InputBaseState::select_right))
.on_action(window.listener_for(
&entity,
InputBaseState::overlay_menu_action::<OverlayMenuNext>,
))
.on_action(window.listener_for(
&entity,
InputBaseState::overlay_menu_action::<OverlayMenuPrevious>,
))
.on_action(window.listener_for(
&entity,
InputBaseState::overlay_menu_action::<OverlayMenuAccept>,
))
.on_action(window.listener_for(
&entity,
InputBaseState::overlay_menu_action::<OverlayMenuDismiss>,
))
.when(self.is_multi_line(), |this| {
this.on_action(window.listener_for(&entity, InputBaseState::up))
.on_action(window.listener_for(&entity, InputBaseState::down))
Expand Down
6 changes: 4 additions & 2 deletions crates/base/src/input/editor/lsp/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,10 @@ impl InputBaseState<EditorMode> {
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let closes_overlay =
crate::input::Enter::is_primary(&*action) || action.partial_eq(&crate::input::Escape);
let closes_overlay = crate::input::Enter::is_primary(&*action)
|| action.partial_eq(&crate::input::Escape)
|| action.partial_eq(&crate::input::OverlayMenuAccept)
|| action.partial_eq(&crate::input::OverlayMenuDismiss);
let kind = if self.extras.context_menu_content.completion.open {
Some(super::InputOverlayKind::Completion)
} else if self.extras.context_menu_content.code_action.open {
Expand Down
64 changes: 38 additions & 26 deletions crates/base/src/input/editor/lsp/overlay.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::*;
use crate::input::EditorMode;
use crate::input::{EditorMode, InputModeKind};
use std::ops::Range;

use lsp_types::{CompletionItem, Hover};
Expand All @@ -22,7 +22,7 @@ impl CompletionMenuState {
self.revision
}

pub(super) fn bump(&mut self) {
pub(crate) fn bump(&mut self) {
self.revision = self.revision.wrapping_add(1);
}
}
Expand Down Expand Up @@ -63,6 +63,41 @@ pub enum InputOverlayKind {
CodeAction,
}

impl<M: InputModeKind> InputBaseState<M> {
pub(crate) fn apply_completion_item(
&mut self,
item: &CompletionItem,
fallback_range: Range<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut range = fallback_range;
let mut new_text = item.label.clone();
if let Some(edit) = item.text_edit.as_ref() {
match edit {
lsp_types::CompletionTextEdit::Edit(edit) => {
new_text.clone_from(&edit.new_text);
range = self.text.position_to_offset(&edit.range.start)
..self.text.position_to_offset(&edit.range.end);
}
lsp_types::CompletionTextEdit::InsertAndReplace(edit) => {
new_text.clone_from(&edit.new_text);
range = self.text.position_to_offset(&edit.replace.start)
..self.text.position_to_offset(&edit.replace.end);
}
}
} else if let Some(insert_text) = item.insert_text.as_ref() {
new_text.clone_from(insert_text);
range = range.end..range.end;
}
self.completion_inserting = true;
let range = self.range_to_utf16(&range);
self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
self.completion_inserting = false;
self.focus(window, cx);
}
}

impl InputBaseState<EditorMode> {
pub fn present_completion_items(
&mut self,
Expand Down Expand Up @@ -170,30 +205,7 @@ impl InputBaseState<EditorMode> {
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut range = fallback_range;
let mut new_text = item.label.clone();
if let Some(edit) = item.text_edit.as_ref() {
match edit {
lsp_types::CompletionTextEdit::Edit(edit) => {
new_text.clone_from(&edit.new_text);
range = self.text.position_to_offset(&edit.range.start)
..self.text.position_to_offset(&edit.range.end);
}
lsp_types::CompletionTextEdit::InsertAndReplace(edit) => {
new_text.clone_from(&edit.new_text);
range = self.text.position_to_offset(&edit.replace.start)
..self.text.position_to_offset(&edit.replace.end);
}
}
} else if let Some(insert_text) = item.insert_text.as_ref() {
new_text.clone_from(insert_text);
range = range.end..range.end;
}
self.completion_inserting = true;
let range = self.range_to_utf16(&range);
self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
self.completion_inserting = false;
self.focus(window, cx);
self.apply_completion_item(item, fallback_range, window, cx);
}

#[doc(hidden)]
Expand Down
115 changes: 115 additions & 0 deletions crates/base/src/input/input/completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::ops::Range;

use gpui::{Action, Context, Window};
use lsp_types::CompletionItem;

use super::super::{CompletionMenuState, InputBaseState, InputMode, InputOverlayKind};

impl InputBaseState<InputMode> {
/// Shows `items` in the completion menu, anchored at the caret.
///
/// `trigger_start_offset` is where the text each item replaces begins and
/// `query` is what the user has typed since. Items normally carry their own
/// `text_edit`; otherwise the span from the start offset to the caret is
/// replaced. An empty list closes the menu.
pub fn present_completion_items(
&mut self,
trigger_start_offset: usize,
query: impl Into<String>,
items: Vec<CompletionItem>,
cx: &mut Context<Self>,
) {
let completion = &mut self.extras.completion;
completion.trigger_start_offset = Some(trigger_start_offset);
completion.query = query.into();
completion.items = items;
completion.open = !completion.items.is_empty();
completion.bump();
cx.notify();
}

pub fn dismiss_completion_overlay(&mut self, cx: &mut Context<Self>) {
if self.extras.completion.open {
self.extras.completion.open = false;
cx.notify();
}
}

pub fn insert_completion(
&mut self,
item: &CompletionItem,
fallback_range: Range<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.apply_completion_item(item, fallback_range, window, cx);
}

#[doc(hidden)]
pub fn completion_menu_state(&self) -> &CompletionMenuState {
&self.extras.completion
}

#[doc(hidden)]
pub fn route_overlay_action(
&mut self,
action: Box<dyn Action>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.handle_action_for_context_menu(action, window, cx)
}

pub fn set_overlay_action_handler(
&mut self,
handler: impl Fn(
InputOverlayKind,
Box<dyn Action>,
&mut Window,
&mut Context<InputBaseState<InputMode>>,
) -> bool
+ 'static,
) {
self.overlay_action_handler = Some(std::rc::Rc::new(handler));
}

pub fn has_overlay_action_handler(&self) -> bool {
self.overlay_action_handler.is_some()
}

pub(crate) fn hide_context_menu(&mut self, cx: &mut Context<Self>) {
// Runs on every edit and caret move of every plain input; stay quiet
// unless there is a menu to close.
if self.extras.completion.open {
self.extras.completion.open = false;
cx.notify();
}
}

pub(crate) fn is_context_menu_open(&self) -> bool {
self.extras.completion.open
}

pub(crate) fn handle_action_for_context_menu(
&mut self,
action: Box<dyn Action>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
if !self.extras.completion.open {
return false;
}
let Some(handler) = self.overlay_action_handler.clone() else {
return false;
};
let closes_overlay = crate::input::Enter::is_primary(&*action)
|| action.partial_eq(&crate::input::Escape)
|| action.partial_eq(&crate::input::OverlayMenuAccept)
|| action.partial_eq(&crate::input::OverlayMenuDismiss);
let handled = handler(InputOverlayKind::Completion, action, window, cx);
if handled && closes_overlay {
self.hide_context_menu(cx);
}
handled
}
}
2 changes: 2 additions & 0 deletions crates/base/src/input/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use gpui::{App, Entity, IntoElement, RenderOnce, Window};

use super::{InputBaseState, InputMode};

mod completion;

/// State for a single-line text input.
///
/// This is the shared editing engine in its single-line kind. Multi-line
Expand Down
3 changes: 2 additions & 1 deletion crates/base/src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ pub use highlighting::{
pub use indent::TabSize;
pub use input::{Input, InputState};
pub use kind::{
EditorExtras, EditorMode, InputExtras, InputMode, InputModeKind, MultiLineMode, TextareaMode,
EditorExtras, EditorMode, FieldExtras, InputExtras, InputMode, InputModeKind, MultiLineMode,
TextareaMode,
};
pub(crate) use language::EditorLanguage;
pub use language::{LanguageProvider, set_language_config, set_language_provider};
Expand Down
Loading
Loading