Don't hide inline assist when editor loses focus (#12990)

Release Notes:

- Now when an editor loses focus (e.g. from switching tabs) and then
gains focus again, it doesn't close the inline assist. Instead, it only
closes when you move the cursor outside of it, e.g. by clicking
somewhere else in its parent editor.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Richard Feldman 2024-06-17 03:43:52 -04:00 committed by GitHub
parent 15d3e54ae3
commit 4855da53df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 90 additions and 81 deletions

View File

@ -29,7 +29,7 @@ use futures::future::Shared;
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use gpui::{ use gpui::{
div, point, rems, Action, AnyElement, AnyView, AppContext, AsyncAppContext, AsyncWindowContext, div, point, rems, Action, AnyElement, AnyView, AppContext, AsyncAppContext, AsyncWindowContext,
ClipboardItem, Context as _, Empty, EventEmitter, FocusHandle, FocusableView, ClipboardItem, Context as _, Empty, EventEmitter, FocusHandle, FocusOutEvent, FocusableView,
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, UpdateGlobal, View, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, UpdateGlobal, View,
ViewContext, VisualContext, WeakView, WindowContext, ViewContext, VisualContext, WeakView, WindowContext,
@ -296,7 +296,7 @@ impl AssistantPanel {
} }
} }
fn focus_out(&mut self, cx: &mut ViewContext<Self>) { fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
self.toolbar self.toolbar
.update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx));
cx.notify(); cx.notify();

View File

@ -277,19 +277,19 @@ impl InlineAssistant {
) { ) {
let assist_id = inline_assist_editor.read(cx).id; let assist_id = inline_assist_editor.read(cx).id;
match event { match event {
InlineAssistEditorEvent::Started => { InlineAssistEditorEvent::StartRequested => {
self.start_inline_assist(assist_id, cx); self.start_inline_assist(assist_id, cx);
} }
InlineAssistEditorEvent::Stopped => { InlineAssistEditorEvent::StopRequested => {
self.stop_inline_assist(assist_id, cx); self.stop_inline_assist(assist_id, cx);
} }
InlineAssistEditorEvent::Confirmed => { InlineAssistEditorEvent::ConfirmRequested => {
self.finish_inline_assist(assist_id, false, cx); self.finish_inline_assist(assist_id, false, cx);
} }
InlineAssistEditorEvent::Canceled => { InlineAssistEditorEvent::CancelRequested => {
self.finish_inline_assist(assist_id, true, cx); self.finish_inline_assist(assist_id, true, cx);
} }
InlineAssistEditorEvent::Dismissed => { InlineAssistEditorEvent::DismissRequested => {
self.dismiss_inline_assist(assist_id, cx); self.dismiss_inline_assist(assist_id, cx);
} }
InlineAssistEditorEvent::Resized { height_in_lines } => { InlineAssistEditorEvent::Resized { height_in_lines } => {
@ -345,14 +345,8 @@ impl InlineAssistant {
match event { match event {
EditorEvent::SelectionsChanged { local } if *local => { EditorEvent::SelectionsChanged { local } if *local => {
if let Some(decorations) = assist.editor_decorations.as_ref() { if let CodegenStatus::Idle = &assist.codegen.read(cx).status {
if decorations self.finish_inline_assist(assist_id, true, cx);
.prompt_editor
.focus_handle(cx)
.contains_focused(cx)
{
cx.focus_view(&editor);
}
} }
} }
EditorEvent::Saved => { EditorEvent::Saved => {
@ -813,11 +807,11 @@ impl InlineAssistId {
} }
enum InlineAssistEditorEvent { enum InlineAssistEditorEvent {
Started, StartRequested,
Stopped, StopRequested,
Confirmed, ConfirmRequested,
Canceled, CancelRequested,
Dismissed, DismissRequested,
Resized { height_in_lines: u8 }, Resized { height_in_lines: u8 },
} }
@ -850,15 +844,17 @@ impl Render for InlineAssistEditor {
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.tooltip(|cx| Tooltip::for_action("Transform", &menu::Confirm, cx)) .tooltip(|cx| Tooltip::for_action("Transform", &menu::Confirm, cx))
.on_click( .on_click(
cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Started)), cx.listener(|_, _, cx| {
cx.emit(InlineAssistEditorEvent::StartRequested)
}),
), ),
IconButton::new("cancel", IconName::Close) IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.size(ButtonSize::None) .size(ButtonSize::None)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx)) .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click( .on_click(cx.listener(|_, _, cx| {
cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)), cx.emit(InlineAssistEditorEvent::CancelRequested)
), })),
] ]
} }
CodegenStatus::Pending => { CodegenStatus::Pending => {
@ -876,15 +872,15 @@ impl Render for InlineAssistEditor {
) )
}) })
.on_click( .on_click(
cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Stopped)), cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::StopRequested)),
), ),
IconButton::new("cancel", IconName::Close) IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.size(ButtonSize::None) .size(ButtonSize::None)
.tooltip(|cx| Tooltip::text("Cancel Assist", cx)) .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
.on_click( .on_click(cx.listener(|_, _, cx| {
cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)), cx.emit(InlineAssistEditorEvent::CancelRequested)
), })),
] ]
} }
CodegenStatus::Error(_) | CodegenStatus::Done => { CodegenStatus::Error(_) | CodegenStatus::Done => {
@ -903,7 +899,7 @@ impl Render for InlineAssistEditor {
) )
}) })
.on_click(cx.listener(|_, _, cx| { .on_click(cx.listener(|_, _, cx| {
cx.emit(InlineAssistEditorEvent::Started); cx.emit(InlineAssistEditorEvent::StartRequested);
})) }))
} else { } else {
IconButton::new("confirm", IconName::Check) IconButton::new("confirm", IconName::Check)
@ -911,16 +907,16 @@ impl Render for InlineAssistEditor {
.size(ButtonSize::None) .size(ButtonSize::None)
.tooltip(|cx| Tooltip::for_action("Confirm Assist", &menu::Confirm, cx)) .tooltip(|cx| Tooltip::for_action("Confirm Assist", &menu::Confirm, cx))
.on_click(cx.listener(|_, _, cx| { .on_click(cx.listener(|_, _, cx| {
cx.emit(InlineAssistEditorEvent::Confirmed); cx.emit(InlineAssistEditorEvent::ConfirmRequested);
})) }))
}, },
IconButton::new("cancel", IconName::Close) IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.size(ButtonSize::None) .size(ButtonSize::None)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx)) .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click( .on_click(cx.listener(|_, _, cx| {
cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)), cx.emit(InlineAssistEditorEvent::CancelRequested)
), })),
] ]
} }
}; };
@ -1100,23 +1096,6 @@ impl InlineAssistEditor {
self.edited_since_done = true; self.edited_since_done = true;
cx.notify(); cx.notify();
} }
EditorEvent::Blurred => {
if let CodegenStatus::Idle = &self.codegen.read(cx).status {
let assistant_panel_is_focused = self
.workspace
.as_ref()
.and_then(|workspace| {
let panel =
workspace.upgrade()?.read(cx).panel::<AssistantPanel>(cx)?;
Some(panel.focus_handle(cx).contains_focused(cx))
})
.unwrap_or(false);
if !assistant_panel_is_focused {
cx.emit(InlineAssistEditorEvent::Canceled);
}
}
}
_ => {} _ => {}
} }
} }
@ -1142,10 +1121,10 @@ impl InlineAssistEditor {
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status { match &self.codegen.read(cx).status {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => { CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
cx.emit(InlineAssistEditorEvent::Canceled); cx.emit(InlineAssistEditorEvent::CancelRequested);
} }
CodegenStatus::Pending => { CodegenStatus::Pending => {
cx.emit(InlineAssistEditorEvent::Stopped); cx.emit(InlineAssistEditorEvent::StopRequested);
} }
} }
} }
@ -1153,16 +1132,16 @@ impl InlineAssistEditor {
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status { match &self.codegen.read(cx).status {
CodegenStatus::Idle => { CodegenStatus::Idle => {
cx.emit(InlineAssistEditorEvent::Started); cx.emit(InlineAssistEditorEvent::StartRequested);
} }
CodegenStatus::Pending => { CodegenStatus::Pending => {
cx.emit(InlineAssistEditorEvent::Dismissed); cx.emit(InlineAssistEditorEvent::DismissRequested);
} }
CodegenStatus::Done | CodegenStatus::Error(_) => { CodegenStatus::Done | CodegenStatus::Error(_) => {
if self.edited_since_done { if self.edited_since_done {
cx.emit(InlineAssistEditorEvent::Started); cx.emit(InlineAssistEditorEvent::StartRequested);
} else { } else {
cx.emit(InlineAssistEditorEvent::Confirmed); cx.emit(InlineAssistEditorEvent::ConfirmRequested);
} }
} }
} }

View File

@ -150,7 +150,7 @@ impl ProjectDiagnosticsEditor {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx)) cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))
.detach(); .detach();
cx.on_focus_out(&focus_handle, |this, cx| this.focus_out(cx)) cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))
.detach(); .detach();
let excerpts = cx.new_model(|cx| { let excerpts = cx.new_model(|cx| {

View File

@ -66,11 +66,12 @@ use git::diff_hunk_to_display;
use gpui::{ use gpui::{
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement, div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, FocusableView,
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, ListSizingBehavior, Model, FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle,
View, ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle,
WeakView, WhiteSpace, WindowContext,
}; };
use highlight_matching_bracket::refresh_matching_bracket_highlights; use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState}; use hover_popover::{hide_hover, HoverState};
@ -448,6 +449,7 @@ struct BufferOffset(usize);
/// See the [module level documentation](self) for more information. /// See the [module level documentation](self) for more information.
pub struct Editor { pub struct Editor {
focus_handle: FocusHandle, focus_handle: FocusHandle,
last_focused: Option<WeakFocusHandle>,
/// The text buffer being edited /// The text buffer being edited
buffer: Model<MultiBuffer>, buffer: Model<MultiBuffer>,
/// Map of how text in the buffer should be displayed. /// Map of how text in the buffer should be displayed.
@ -1735,6 +1737,8 @@ impl Editor {
); );
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, Self::handle_focus).detach(); cx.on_focus(&focus_handle, Self::handle_focus).detach();
cx.on_focus_out(&focus_handle, Self::handle_focus_out)
.detach();
cx.on_blur(&focus_handle, Self::handle_blur).detach(); cx.on_blur(&focus_handle, Self::handle_blur).detach();
let show_indent_guides = if mode == EditorMode::SingleLine { let show_indent_guides = if mode == EditorMode::SingleLine {
@ -1745,6 +1749,7 @@ impl Editor {
let mut this = Self { let mut this = Self {
focus_handle, focus_handle,
last_focused: None,
buffer: buffer.clone(), buffer: buffer.clone(),
display_map: display_map.clone(), display_map: display_map.clone(),
selections, selections,
@ -11315,9 +11320,13 @@ impl Editor {
fn handle_focus(&mut self, cx: &mut ViewContext<Self>) { fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(EditorEvent::Focused); cx.emit(EditorEvent::Focused);
if let Some(rename) = self.pending_rename.as_ref() {
let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone(); if let Some(last_focused) = self
cx.focus(&rename_editor_focus_handle); .last_focused
.take()
.and_then(|last_focused| last_focused.upgrade())
{
cx.focus(&last_focused);
} else { } else {
if let Some(blame) = self.blame.as_ref() { if let Some(blame) = self.blame.as_ref() {
blame.update(cx, GitBlame::focus) blame.update(cx, GitBlame::focus)
@ -11339,6 +11348,10 @@ impl Editor {
} }
} }
fn handle_focus_out(&mut self, event: FocusOutEvent, _cx: &mut ViewContext<Self>) {
self.last_focused = Some(event.blurred);
}
pub fn handle_blur(&mut self, cx: &mut ViewContext<Self>) { pub fn handle_blur(&mut self, cx: &mut ViewContext<Self>) {
self.blink_manager.update(cx, BlinkManager::disable); self.blink_manager.update(cx, BlinkManager::disable);
self.buffer self.buffer

View File

@ -85,13 +85,20 @@ impl DispatchPhase {
type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>; type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>; type AnyWindowFocusListener =
Box<dyn FnMut(&WindowFocusEvent, &mut WindowContext) -> bool + 'static>;
struct FocusEvent { struct WindowFocusEvent {
previous_focus_path: SmallVec<[FocusId; 8]>, previous_focus_path: SmallVec<[FocusId; 8]>,
current_focus_path: SmallVec<[FocusId; 8]>, current_focus_path: SmallVec<[FocusId; 8]>,
} }
/// This is provided when subscribing for `ViewContext::on_focus_out` events.
pub struct FocusOutEvent {
/// A weak focus handle representing what was blurred.
pub blurred: WeakFocusHandle,
}
slotmap::new_key_type! { slotmap::new_key_type! {
/// A globally unique identifier for a focusable element. /// A globally unique identifier for a focusable element.
pub struct FocusId; pub struct FocusId;
@ -1397,7 +1404,7 @@ impl<'a> WindowContext<'a> {
.retain(&(), |listener| listener(self)); .retain(&(), |listener| listener(self));
} }
let event = FocusEvent { let event = WindowFocusEvent {
previous_focus_path: if previous_window_active { previous_focus_path: if previous_window_active {
previous_focus_path previous_focus_path
} else { } else {
@ -4055,6 +4062,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
} }
/// Register a listener to be called when the given focus handle or one of its descendants receives focus. /// Register a listener to be called when the given focus handle or one of its descendants receives focus.
/// This does not fire if the given focus handle - or one of its descendants - was previously focused.
/// Returns a subscription and persists until the subscription is dropped. /// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_in( pub fn on_focus_in(
&mut self, &mut self,
@ -4124,17 +4132,25 @@ impl<'a, V: 'static> ViewContext<'a, V> {
pub fn on_focus_out( pub fn on_focus_out(
&mut self, &mut self,
handle: &FocusHandle, handle: &FocusHandle,
mut listener: impl FnMut(&mut V, &mut ViewContext<V>) + 'static, mut listener: impl FnMut(&mut V, FocusOutEvent, &mut ViewContext<V>) + 'static,
) -> Subscription { ) -> Subscription {
let view = self.view.downgrade(); let view = self.view.downgrade();
let focus_id = handle.id; let focus_id = handle.id;
let (subscription, activate) = let (subscription, activate) =
self.window.new_focus_listener(Box::new(move |event, cx| { self.window.new_focus_listener(Box::new(move |event, cx| {
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
if event.previous_focus_path.contains(&focus_id) if let Some(blurred_id) = event.previous_focus_path.last().copied() {
&& !event.current_focus_path.contains(&focus_id) if event.previous_focus_path.contains(&focus_id)
{ && !event.current_focus_path.contains(&focus_id)
listener(view, cx) {
let event = FocusOutEvent {
blurred: WeakFocusHandle {
id: blurred_id,
handles: Arc::downgrade(&cx.window.focus_handles),
},
};
listener(view, event, cx)
}
} }
}) })
.is_ok() .is_ok()
@ -4193,7 +4209,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}); });
} }
/// Emit an event to be handled any other views that have subscribed via [ViewContext::subscribe]. /// Emit an event to be handled by any other views that have subscribed via [ViewContext::subscribe].
pub fn emit<Evt>(&mut self, event: Evt) pub fn emit<Evt>(&mut self, event: Evt)
where where
Evt: 'static, Evt: 'static,

View File

@ -152,7 +152,7 @@ impl TerminalView {
let focus_in = cx.on_focus_in(&focus_handle, |terminal_view, cx| { let focus_in = cx.on_focus_in(&focus_handle, |terminal_view, cx| {
terminal_view.focus_in(cx); terminal_view.focus_in(cx);
}); });
let focus_out = cx.on_focus_out(&focus_handle, |terminal_view, cx| { let focus_out = cx.on_focus_out(&focus_handle, |terminal_view, _event, cx| {
terminal_view.focus_out(cx); terminal_view.focus_out(cx);
}); });

View File

@ -87,7 +87,7 @@ impl ModalLayer {
cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| { cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| {
this.hide_modal(cx); this.hide_modal(cx);
}), }),
cx.on_focus_out(&focus_handle, |this, cx| { cx.on_focus_out(&focus_handle, |this, _event, cx| {
if this.dismiss_on_focus_lost { if this.dismiss_on_focus_lost {
this.hide_modal(cx); this.hide_modal(cx);
} }

View File

@ -14,9 +14,10 @@ use futures::{stream::FuturesUnordered, StreamExt};
use gpui::{ use gpui::{
actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement, actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
AppContext, AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId, AppContext, AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId,
EventEmitter, ExternalPaths, FocusHandle, FocusableView, KeyContext, Model, MouseButton, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext, Model,
MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render, ScrollHandle, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView, WindowContext, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView,
WindowContext,
}; };
use itertools::Itertools; use itertools::Itertools;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -517,7 +518,7 @@ impl Pane {
.map_or(false, |menu| menu.focus_handle(cx).is_focused(cx)) .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
} }
fn focus_out(&mut self, cx: &mut ViewContext<Self>) { fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
self.was_focused = false; self.was_focused = false;
self.toolbar.update(cx, |toolbar, cx| { self.toolbar.update(cx, |toolbar, cx| {
toolbar.focus_changed(false, cx); toolbar.focus_changed(false, cx);

View File

@ -2214,7 +2214,7 @@ mod tests {
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
window window
.read_with(cx, |workspace, cx| { .update(cx, |workspace, cx| {
assert_eq!(workspace.panes().len(), 1); assert_eq!(workspace.panes().len(), 1);
assert!(workspace.active_item(cx).is_none()); assert!(workspace.active_item(cx).is_none());
}) })