Refine inline transformation UX (#12939)

https://github.com/zed-industries/zed/assets/482957/1790e32e-1f59-4831-8a4c-722cf441e7e9



Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2024-06-13 08:35:22 +02:00 committed by GitHub
parent 9e3c5f3e12
commit e1f4dfc068
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 419 additions and 219 deletions

View File

@ -79,7 +79,6 @@ pub fn init(cx: &mut AppContext) {
workspace.toggle_panel_focus::<AssistantPanel>(cx); workspace.toggle_panel_focus::<AssistantPanel>(cx);
}) })
.register_action(AssistantPanel::inline_assist) .register_action(AssistantPanel::inline_assist)
.register_action(AssistantPanel::cancel_last_inline_assist)
.register_action(ContextEditor::quote_selection); .register_action(ContextEditor::quote_selection);
}, },
) )
@ -421,19 +420,6 @@ impl AssistantPanel {
} }
} }
fn cancel_last_inline_assist(
_workspace: &mut Workspace,
_: &editor::actions::Cancel,
cx: &mut ViewContext<Workspace>,
) {
let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
assistant.cancel_last_inline_assist(cx)
});
if !canceled {
cx.propagate();
}
}
fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> { fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
let workspace = self.workspace.upgrade()?; let workspace = self.workspace.upgrade()?;

View File

@ -16,8 +16,8 @@ use editor::{
}; };
use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{ use gpui::{
AnyWindowHandle, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
Global, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View,
ViewContext, WeakView, WhiteSpace, WindowContext, ViewContext, WeakView, WhiteSpace, WindowContext,
}; };
use language::{Buffer, Point, TransactionId}; use language::{Buffer, Point, TransactionId};
@ -34,6 +34,7 @@ use std::{
}; };
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, Tooltip}; use ui::{prelude::*, Tooltip};
use util::RangeExt;
use workspace::{notifications::NotificationId, Toast, Workspace}; use workspace::{notifications::NotificationId, Toast, Workspace};
pub fn init(telemetry: Arc<Telemetry>, cx: &mut AppContext) { pub fn init(telemetry: Arc<Telemetry>, cx: &mut AppContext) {
@ -45,16 +46,11 @@ const PROMPT_HISTORY_MAX_LEN: usize = 20;
pub struct InlineAssistant { pub struct InlineAssistant {
next_assist_id: InlineAssistId, next_assist_id: InlineAssistId,
pending_assists: HashMap<InlineAssistId, PendingInlineAssist>, pending_assists: HashMap<InlineAssistId, PendingInlineAssist>,
pending_assist_ids_by_editor: HashMap<WeakView<Editor>, EditorPendingAssists>, pending_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<InlineAssistId>>,
prompt_history: VecDeque<String>, prompt_history: VecDeque<String>,
telemetry: Option<Arc<Telemetry>>, telemetry: Option<Arc<Telemetry>>,
} }
struct EditorPendingAssists {
window: AnyWindowHandle,
assist_ids: Vec<InlineAssistId>,
}
impl Global for InlineAssistant {} impl Global for InlineAssistant {}
impl InlineAssistant { impl InlineAssistant {
@ -103,7 +99,7 @@ impl InlineAssistant {
} }
}; };
let inline_assist_id = self.next_assist_id.post_inc(); let assist_id = self.next_assist_id.post_inc();
let codegen = cx.new_model(|cx| { let codegen = cx.new_model(|cx| {
Codegen::new( Codegen::new(
editor.read(cx).buffer().clone(), editor.read(cx).buffer().clone(),
@ -116,7 +112,7 @@ impl InlineAssistant {
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
let prompt_editor = cx.new_view(|cx| { let prompt_editor = cx.new_view(|cx| {
InlineAssistEditor::new( InlineAssistEditor::new(
inline_assist_id, assist_id,
gutter_dimensions.clone(), gutter_dimensions.clone(),
self.prompt_history.clone(), self.prompt_history.clone(),
codegen.clone(), codegen.clone(),
@ -164,7 +160,7 @@ impl InlineAssistant {
}); });
self.pending_assists.insert( self.pending_assists.insert(
inline_assist_id, assist_id,
PendingInlineAssist { PendingInlineAssist {
include_context, include_context,
editor: editor.downgrade(), editor: editor.downgrade(),
@ -179,24 +175,35 @@ impl InlineAssistant {
_subscriptions: vec![ _subscriptions: vec![
cx.subscribe(&prompt_editor, |inline_assist_editor, event, cx| { cx.subscribe(&prompt_editor, |inline_assist_editor, event, cx| {
InlineAssistant::update_global(cx, |this, cx| { InlineAssistant::update_global(cx, |this, cx| {
this.handle_inline_assistant_event(inline_assist_editor, event, cx) this.handle_inline_assistant_editor_event(
inline_assist_editor,
event,
cx,
)
}) })
}), }),
cx.subscribe(editor, { editor.update(cx, |editor, _cx| {
let inline_assist_editor = prompt_editor.downgrade(); editor.register_action(
move |editor, event, cx| { move |_: &editor::actions::Newline, cx: &mut WindowContext| {
if let Some(inline_assist_editor) = inline_assist_editor.upgrade() { InlineAssistant::update_global(cx, |this, cx| {
if let EditorEvent::SelectionsChanged { local } = event { this.handle_editor_action(assist_id, false, cx)
if *local })
&& inline_assist_editor },
.focus_handle(cx) )
.contains_focused(cx) }),
{ editor.update(cx, |editor, _cx| {
cx.focus_view(&editor); editor.register_action(
} move |_: &editor::actions::Cancel, cx: &mut WindowContext| {
} InlineAssistant::update_global(cx, |this, cx| {
} this.handle_editor_action(assist_id, true, cx)
} })
},
)
}),
cx.subscribe(editor, move |editor, event, cx| {
InlineAssistant::update_global(cx, |this, cx| {
this.handle_editor_event(assist_id, editor, event, cx)
})
}), }),
cx.observe(&codegen, { cx.observe(&codegen, {
let editor = editor.downgrade(); let editor = editor.downgrade();
@ -204,19 +211,17 @@ impl InlineAssistant {
if let Some(editor) = editor.upgrade() { if let Some(editor) = editor.upgrade() {
InlineAssistant::update_global(cx, |this, cx| { InlineAssistant::update_global(cx, |this, cx| {
this.update_editor_highlights(&editor, cx); this.update_editor_highlights(&editor, cx);
this.update_editor_blocks(&editor, inline_assist_id, cx); this.update_editor_blocks(&editor, assist_id, cx);
}) })
} }
} }
}), }),
cx.subscribe(&codegen, move |codegen, event, cx| { cx.subscribe(&codegen, move |codegen, event, cx| {
InlineAssistant::update_global(cx, |this, cx| match event { InlineAssistant::update_global(cx, |this, cx| match event {
CodegenEvent::Undone => { CodegenEvent::Undone => this.finish_inline_assist(assist_id, false, cx),
this.finish_inline_assist(inline_assist_id, false, cx)
}
CodegenEvent::Finished => { CodegenEvent::Finished => {
let pending_assist = if let Some(pending_assist) = let pending_assist = if let Some(pending_assist) =
this.pending_assists.get(&inline_assist_id) this.pending_assists.get(&assist_id)
{ {
pending_assist pending_assist
} else { } else {
@ -238,7 +243,7 @@ impl InlineAssistant {
let id = NotificationId::identified::< let id = NotificationId::identified::<
InlineAssistantError, InlineAssistantError,
>( >(
inline_assist_id.0 assist_id.0
); );
workspace.show_toast(Toast::new(id, error), cx); workspace.show_toast(Toast::new(id, error), cx);
@ -248,7 +253,7 @@ impl InlineAssistant {
} }
if pending_assist.editor_decorations.is_none() { if pending_assist.editor_decorations.is_none() {
this.finish_inline_assist(inline_assist_id, false, cx); this.finish_inline_assist(assist_id, false, cx);
} }
} }
}) })
@ -259,16 +264,12 @@ impl InlineAssistant {
self.pending_assist_ids_by_editor self.pending_assist_ids_by_editor
.entry(editor.downgrade()) .entry(editor.downgrade())
.or_insert_with(|| EditorPendingAssists { .or_default()
window: cx.window_handle(), .push(assist_id);
assist_ids: Vec::new(),
})
.assist_ids
.push(inline_assist_id);
self.update_editor_highlights(editor, cx); self.update_editor_highlights(editor, cx);
} }
fn handle_inline_assistant_event( fn handle_inline_assistant_editor_event(
&mut self, &mut self,
inline_assist_editor: View<InlineAssistEditor>, inline_assist_editor: View<InlineAssistEditor>,
event: &InlineAssistEditorEvent, event: &InlineAssistEditorEvent,
@ -289,7 +290,7 @@ impl InlineAssistant {
self.finish_inline_assist(assist_id, true, cx); self.finish_inline_assist(assist_id, true, cx);
} }
InlineAssistEditorEvent::Dismissed => { InlineAssistEditorEvent::Dismissed => {
self.hide_inline_assist_decorations(assist_id, cx); self.dismiss_inline_assist(assist_id, cx);
} }
InlineAssistEditorEvent::Resized { height_in_lines } => { InlineAssistEditorEvent::Resized { height_in_lines } => {
self.resize_inline_assist(assist_id, *height_in_lines, cx); self.resize_inline_assist(assist_id, *height_in_lines, cx);
@ -297,20 +298,87 @@ impl InlineAssistant {
} }
} }
pub fn cancel_last_inline_assist(&mut self, cx: &mut WindowContext) -> bool { fn handle_editor_action(
for (editor, pending_assists) in &self.pending_assist_ids_by_editor { &mut self,
if pending_assists.window == cx.window_handle() { assist_id: InlineAssistId,
if let Some(editor) = editor.upgrade() { undo: bool,
if editor.read(cx).is_focused(cx) { cx: &mut WindowContext,
if let Some(assist_id) = pending_assists.assist_ids.last().copied() { ) {
self.finish_inline_assist(assist_id, true, cx); let Some(assist) = self.pending_assists.get(&assist_id) else {
return true; return;
} };
let Some(editor) = assist.editor.upgrade() else {
return;
};
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
let assist_range = assist.codegen.read(cx).range().to_offset(&buffer);
let editor = editor.read(cx);
if editor.selections.count() == 1 {
let selection = editor.selections.newest::<usize>(cx);
if assist_range.contains(&selection.start) && assist_range.contains(&selection.end) {
if undo {
self.finish_inline_assist(assist_id, true, cx);
} else if matches!(assist.codegen.read(cx).status, CodegenStatus::Pending) {
self.dismiss_inline_assist(assist_id, cx);
} else {
self.finish_inline_assist(assist_id, false, cx);
}
return;
}
}
cx.propagate();
}
fn handle_editor_event(
&mut self,
assist_id: InlineAssistId,
editor: View<Editor>,
event: &EditorEvent,
cx: &mut WindowContext,
) {
let Some(assist) = self.pending_assists.get(&assist_id) else {
return;
};
match event {
EditorEvent::SelectionsChanged { local } if *local => {
if let Some(decorations) = assist.editor_decorations.as_ref() {
if decorations
.prompt_editor
.focus_handle(cx)
.contains_focused(cx)
{
cx.focus_view(&editor);
} }
} }
} }
EditorEvent::Saved => {
if let CodegenStatus::Done = &assist.codegen.read(cx).status {
self.finish_inline_assist(assist_id, false, cx)
}
}
EditorEvent::Edited { transaction_id }
if matches!(
assist.codegen.read(cx).status,
CodegenStatus::Error(_) | CodegenStatus::Done
) =>
{
let buffer = editor.read(cx).buffer().read(cx);
let edited_ranges =
buffer.edited_ranges_for_transaction::<usize>(*transaction_id, cx);
let assist_range = assist.codegen.read(cx).range().to_offset(&buffer.read(cx));
if edited_ranges
.iter()
.any(|range| range.overlaps(&assist_range))
{
self.finish_inline_assist(assist_id, false, cx);
}
}
_ => {}
} }
false
} }
fn finish_inline_assist( fn finish_inline_assist(
@ -319,15 +387,15 @@ impl InlineAssistant {
undo: bool, undo: bool,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
self.hide_inline_assist_decorations(assist_id, cx); self.dismiss_inline_assist(assist_id, cx);
if let Some(pending_assist) = self.pending_assists.remove(&assist_id) { if let Some(pending_assist) = self.pending_assists.remove(&assist_id) {
if let hash_map::Entry::Occupied(mut entry) = self if let hash_map::Entry::Occupied(mut entry) = self
.pending_assist_ids_by_editor .pending_assist_ids_by_editor
.entry(pending_assist.editor.clone()) .entry(pending_assist.editor.clone())
{ {
entry.get_mut().assist_ids.retain(|id| *id != assist_id); entry.get_mut().retain(|id| *id != assist_id);
if entry.get().assist_ids.is_empty() { if entry.get().is_empty() {
entry.remove(); entry.remove();
} }
} }
@ -344,11 +412,7 @@ impl InlineAssistant {
} }
} }
fn hide_inline_assist_decorations( fn dismiss_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
&mut self,
assist_id: InlineAssistId,
cx: &mut WindowContext,
) -> bool {
let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else { let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
return false; return false;
}; };
@ -558,16 +622,14 @@ impl InlineAssistant {
let mut gutter_transformed_ranges = Vec::new(); let mut gutter_transformed_ranges = Vec::new();
let mut foreground_ranges = Vec::new(); let mut foreground_ranges = Vec::new();
let mut inserted_row_ranges = Vec::new(); let mut inserted_row_ranges = Vec::new();
let empty_inline_assist_ids = Vec::new(); let empty_assist_ids = Vec::new();
let inline_assist_ids = self let assist_ids = self
.pending_assist_ids_by_editor .pending_assist_ids_by_editor
.get(&editor.downgrade()) .get(&editor.downgrade())
.map_or(&empty_inline_assist_ids, |pending_assists| { .unwrap_or(&empty_assist_ids);
&pending_assists.assist_ids
});
for inline_assist_id in inline_assist_ids { for assist_id in assist_ids {
if let Some(pending_assist) = self.pending_assists.get(inline_assist_id) { if let Some(pending_assist) = self.pending_assists.get(assist_id) {
let codegen = pending_assist.codegen.read(cx); let codegen = pending_assist.codegen.read(cx);
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned()); foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
@ -1025,7 +1087,7 @@ impl InlineAssistEditor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
EditorEvent::Edited => { EditorEvent::Edited { .. } => {
let prompt = self.prompt_editor.read(cx).text(cx); let prompt = self.prompt_editor.read(cx).text(cx);
if self if self
.prompt_history_ix .prompt_history_ix

View File

@ -592,19 +592,6 @@ impl PromptLibrary {
} }
} }
fn cancel_last_inline_assist(
&mut self,
_: &editor::actions::Cancel,
cx: &mut ViewContext<Self>,
) {
let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
assistant.cancel_last_inline_assist(cx)
});
if !canceled {
cx.propagate();
}
}
fn handle_prompt_editor_event( fn handle_prompt_editor_event(
&mut self, &mut self,
prompt_id: PromptId, prompt_id: PromptId,
@ -743,7 +730,6 @@ impl PromptLibrary {
div() div()
.on_action(cx.listener(Self::focus_picker)) .on_action(cx.listener(Self::focus_picker))
.on_action(cx.listener(Self::inline_assist)) .on_action(cx.listener(Self::inline_assist))
.on_action(cx.listener(Self::cancel_last_inline_assist))
.flex_grow() .flex_grow()
.h_full() .h_full()
.pt(Spacing::XXLarge.rems(cx)) .pt(Spacing::XXLarge.rems(cx))

View File

@ -232,7 +232,7 @@ impl ChannelView {
this.focus_position_from_link(position.clone(), false, cx); this.focus_position_from_link(position.clone(), false, cx);
this._reparse_subscription.take(); this._reparse_subscription.take();
} }
EditorEvent::Edited | EditorEvent::SelectionsChanged { local: true } => { EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
this._reparse_subscription.take(); this._reparse_subscription.take();
} }
_ => {} _ => {}

View File

@ -116,15 +116,16 @@ use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore}; use settings::{update_settings_file, Settings, SettingsStore};
use smallvec::SmallVec; use smallvec::SmallVec;
use snippet::Snippet; use snippet::Snippet;
use std::ops::Not as _;
use std::{ use std::{
any::TypeId, any::TypeId,
borrow::Cow, borrow::Cow,
cell::RefCell,
cmp::{self, Ordering, Reverse}, cmp::{self, Ordering, Reverse},
mem, mem,
num::NonZeroU32, num::NonZeroU32,
ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
path::Path, path::Path,
rc::Rc,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -377,6 +378,19 @@ impl Default for EditorStyle {
type CompletionId = usize; type CompletionId = usize;
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
struct EditorActionId(usize);
impl EditorActionId {
pub fn post_inc(&mut self) -> Self {
let answer = self.0;
*self = Self(answer + 1);
Self(answer)
}
}
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; // type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>; // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
@ -512,7 +526,8 @@ pub struct Editor {
gutter_dimensions: GutterDimensions, gutter_dimensions: GutterDimensions,
pub vim_replace_map: HashMap<Range<usize>, String>, pub vim_replace_map: HashMap<Range<usize>, String>,
style: Option<EditorStyle>, style: Option<EditorStyle>,
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>, next_editor_action_id: EditorActionId,
editor_actions: Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut ViewContext<Self>)>>>>,
use_autoclose: bool, use_autoclose: bool,
auto_replace_emoji_shortcode: bool, auto_replace_emoji_shortcode: bool,
show_git_blame_gutter: bool, show_git_blame_gutter: bool,
@ -1805,7 +1820,8 @@ impl Editor {
style: None, style: None,
show_cursor_names: false, show_cursor_names: false,
hovered_cursors: Default::default(), hovered_cursors: Default::default(),
editor_actions: Default::default(), next_editor_action_id: EditorActionId::default(),
editor_actions: Rc::default(),
vim_replace_map: Default::default(), vim_replace_map: Default::default(),
show_inline_completions: mode == EditorMode::Full, show_inline_completions: mode == EditorMode::Full,
custom_context_menu: None, custom_context_menu: None,
@ -6448,29 +6464,9 @@ impl Editor {
return; return;
} }
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() { if let Some((selections, _)) =
self.change_selections(None, cx, |s| { self.selection_history.transaction(transaction_id).cloned()
s.select_anchors(selections.to_vec());
});
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_inline_completion(true, cx);
cx.emit(EditorEvent::Edited);
cx.emit(EditorEvent::TransactionUndone {
transaction_id: tx_id,
});
}
}
pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
if self.read_only(cx) {
return;
}
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned()
{ {
self.change_selections(None, cx, |s| { self.change_selections(None, cx, |s| {
s.select_anchors(selections.to_vec()); s.select_anchors(selections.to_vec());
@ -6479,7 +6475,28 @@ impl Editor {
self.request_autoscroll(Autoscroll::fit(), cx); self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx); self.unmark_text(cx);
self.refresh_inline_completion(true, cx); self.refresh_inline_completion(true, cx);
cx.emit(EditorEvent::Edited); cx.emit(EditorEvent::Edited { transaction_id });
cx.emit(EditorEvent::TransactionUndone { transaction_id });
}
}
pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
if self.read_only(cx) {
return;
}
if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
if let Some((_, Some(selections))) =
self.selection_history.transaction(transaction_id).cloned()
{
self.change_selections(None, cx, |s| {
s.select_anchors(selections.to_vec());
});
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_inline_completion(true, cx);
cx.emit(EditorEvent::Edited { transaction_id });
} }
} }
@ -9590,18 +9607,20 @@ impl Editor {
now: Instant, now: Instant,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<TransactionId> { ) -> Option<TransactionId> {
if let Some(tx_id) = self if let Some(transaction_id) = self
.buffer .buffer
.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
{ {
if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) { if let Some((_, end_selections)) =
self.selection_history.transaction_mut(transaction_id)
{
*end_selections = Some(self.selections.disjoint_anchors()); *end_selections = Some(self.selections.disjoint_anchors());
} else { } else {
log::error!("unexpectedly ended a transaction that wasn't started by this editor"); log::error!("unexpectedly ended a transaction that wasn't started by this editor");
} }
cx.emit(EditorEvent::Edited); cx.emit(EditorEvent::Edited { transaction_id });
Some(tx_id) Some(transaction_id)
} else { } else {
None None
} }
@ -11293,21 +11312,28 @@ impl Editor {
pub fn register_action<A: Action>( pub fn register_action<A: Action>(
&mut self, &mut self,
listener: impl Fn(&A, &mut WindowContext) + 'static, listener: impl Fn(&A, &mut WindowContext) + 'static,
) -> &mut Self { ) -> Subscription {
let id = self.next_editor_action_id.post_inc();
let listener = Arc::new(listener); let listener = Arc::new(listener);
self.editor_actions.borrow_mut().insert(
id,
Box::new(move |cx| {
let _view = cx.view().clone();
let cx = cx.window_context();
let listener = listener.clone();
cx.on_action(TypeId::of::<A>(), move |action, phase, cx| {
let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Bubble {
listener(action, cx)
}
})
}),
);
self.editor_actions.push(Box::new(move |cx| { let editor_actions = self.editor_actions.clone();
let _view = cx.view().clone(); Subscription::new(move || {
let cx = cx.window_context(); editor_actions.borrow_mut().remove(&id);
let listener = listener.clone(); })
cx.on_action(TypeId::of::<A>(), move |action, phase, cx| {
let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Bubble {
listener(action, cx)
}
})
}));
self
} }
pub fn file_header_size(&self) -> u8 { pub fn file_header_size(&self) -> u8 {
@ -11764,7 +11790,9 @@ pub enum EditorEvent {
ids: Vec<ExcerptId>, ids: Vec<ExcerptId>,
}, },
BufferEdited, BufferEdited,
Edited, Edited {
transaction_id: clock::Lamport,
},
Reparsed, Reparsed,
Focused, Focused,
Blurred, Blurred,

View File

@ -57,10 +57,10 @@ fn test_edit_events(cx: &mut TestAppContext) {
let events = events.clone(); let events = events.clone();
|cx| { |cx| {
let view = cx.view().clone(); let view = cx.view().clone();
cx.subscribe(&view, move |_, _, event: &EditorEvent, _| { cx.subscribe(&view, move |_, _, event: &EditorEvent, _| match event {
if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
events.borrow_mut().push(("editor1", event.clone())); EditorEvent::BufferEdited => events.borrow_mut().push(("editor1", "buffer edited")),
} _ => {}
}) })
.detach(); .detach();
Editor::for_buffer(buffer.clone(), None, cx) Editor::for_buffer(buffer.clone(), None, cx)
@ -70,11 +70,16 @@ fn test_edit_events(cx: &mut TestAppContext) {
let editor2 = cx.add_window({ let editor2 = cx.add_window({
let events = events.clone(); let events = events.clone();
|cx| { |cx| {
cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| { cx.subscribe(
if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) { &cx.view().clone(),
events.borrow_mut().push(("editor2", event.clone())); move |_, _, event: &EditorEvent, _| match event {
} EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
}) EditorEvent::BufferEdited => {
events.borrow_mut().push(("editor2", "buffer edited"))
}
_ => {}
},
)
.detach(); .detach();
Editor::for_buffer(buffer.clone(), None, cx) Editor::for_buffer(buffer.clone(), None, cx)
} }
@ -87,9 +92,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!( assert_eq!(
mem::take(&mut *events.borrow_mut()), mem::take(&mut *events.borrow_mut()),
[ [
("editor1", EditorEvent::Edited), ("editor1", "edited"),
("editor1", EditorEvent::BufferEdited), ("editor1", "buffer edited"),
("editor2", EditorEvent::BufferEdited), ("editor2", "buffer edited"),
] ]
); );
@ -98,9 +103,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!( assert_eq!(
mem::take(&mut *events.borrow_mut()), mem::take(&mut *events.borrow_mut()),
[ [
("editor2", EditorEvent::Edited), ("editor2", "edited"),
("editor1", EditorEvent::BufferEdited), ("editor1", "buffer edited"),
("editor2", EditorEvent::BufferEdited), ("editor2", "buffer edited"),
] ]
); );
@ -109,9 +114,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!( assert_eq!(
mem::take(&mut *events.borrow_mut()), mem::take(&mut *events.borrow_mut()),
[ [
("editor1", EditorEvent::Edited), ("editor1", "edited"),
("editor1", EditorEvent::BufferEdited), ("editor1", "buffer edited"),
("editor2", EditorEvent::BufferEdited), ("editor2", "buffer edited"),
] ]
); );
@ -120,9 +125,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!( assert_eq!(
mem::take(&mut *events.borrow_mut()), mem::take(&mut *events.borrow_mut()),
[ [
("editor1", EditorEvent::Edited), ("editor1", "edited"),
("editor1", EditorEvent::BufferEdited), ("editor1", "buffer edited"),
("editor2", EditorEvent::BufferEdited), ("editor2", "buffer edited"),
] ]
); );
@ -131,9 +136,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!( assert_eq!(
mem::take(&mut *events.borrow_mut()), mem::take(&mut *events.borrow_mut()),
[ [
("editor2", EditorEvent::Edited), ("editor2", "edited"),
("editor1", EditorEvent::BufferEdited), ("editor1", "buffer edited"),
("editor2", EditorEvent::BufferEdited), ("editor2", "buffer edited"),
] ]
); );
@ -142,9 +147,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!( assert_eq!(
mem::take(&mut *events.borrow_mut()), mem::take(&mut *events.borrow_mut()),
[ [
("editor2", EditorEvent::Edited), ("editor2", "edited"),
("editor1", EditorEvent::BufferEdited), ("editor1", "buffer edited"),
("editor2", EditorEvent::BufferEdited), ("editor2", "buffer edited"),
] ]
); );

View File

@ -153,7 +153,7 @@ impl EditorElement {
fn register_actions(&self, cx: &mut WindowContext) { fn register_actions(&self, cx: &mut WindowContext) {
let view = &self.editor; let view = &self.editor;
view.update(cx, |editor, cx| { view.update(cx, |editor, cx| {
for action in editor.editor_actions.iter() { for action in editor.editor_actions.borrow().values() {
(action)(cx) (action)(cx)
} }
}); });

View File

@ -615,32 +615,36 @@ fn editor_with_deleted_text(
]); ]);
let original_multi_buffer_range = hunk.multi_buffer_range.clone(); let original_multi_buffer_range = hunk.multi_buffer_range.clone();
let diff_base_range = hunk.diff_base_byte_range.clone(); let diff_base_range = hunk.diff_base_byte_range.clone();
editor.register_action::<RevertSelectedHunks>(move |_, cx| { editor
parent_editor .register_action::<RevertSelectedHunks>(move |_, cx| {
.update(cx, |editor, cx| { parent_editor
let Some((buffer, original_text)) = editor.buffer().update(cx, |buffer, cx| { .update(cx, |editor, cx| {
let (_, buffer, _) = let Some((buffer, original_text)) =
buffer.excerpt_containing(original_multi_buffer_range.start, cx)?; editor.buffer().update(cx, |buffer, cx| {
let original_text = let (_, buffer, _) = buffer
buffer.read(cx).diff_base()?.slice(diff_base_range.clone()); .excerpt_containing(original_multi_buffer_range.start, cx)?;
Some((buffer, Arc::from(original_text.to_string()))) let original_text =
}) else { buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
return; Some((buffer, Arc::from(original_text.to_string())))
}; })
buffer.update(cx, |buffer, cx| { else {
buffer.edit( return;
Some(( };
original_multi_buffer_range.start.text_anchor buffer.update(cx, |buffer, cx| {
..original_multi_buffer_range.end.text_anchor, buffer.edit(
original_text, Some((
)), original_multi_buffer_range.start.text_anchor
None, ..original_multi_buffer_range.end.text_anchor,
cx, original_text,
) )),
}); None,
}) cx,
.ok(); )
}); });
})
.ok();
})
.detach();
editor editor
}); });

View File

@ -234,7 +234,7 @@ impl FollowableItem for Editor {
fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> { fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
match event { match event {
EditorEvent::Edited => Some(FollowEvent::Unfollow), EditorEvent::Edited { .. } => Some(FollowEvent::Unfollow),
EditorEvent::SelectionsChanged { local } EditorEvent::SelectionsChanged { local }
| EditorEvent::ScrollPositionChanged { local, .. } => { | EditorEvent::ScrollPositionChanged { local, .. } => {
if *local { if *local {

View File

@ -773,7 +773,7 @@ impl ExtensionsPage {
event: &editor::EditorEvent, event: &editor::EditorEvent,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if let editor::EditorEvent::Edited = event { if let editor::EditorEvent::Edited { .. } = event {
self.query_contains_error = false; self.query_contains_error = false;
self.fetch_extensions_debounced(cx); self.fetch_extensions_debounced(cx);
} }

View File

@ -193,7 +193,7 @@ impl FeedbackModal {
}); });
cx.subscribe(&feedback_editor, |this, editor, event: &EditorEvent, cx| { cx.subscribe(&feedback_editor, |this, editor, event: &EditorEvent, cx| {
if *event == EditorEvent::Edited { if matches!(event, EditorEvent::Edited { .. }) {
this.character_count = editor this.character_count = editor
.read(cx) .read(cx)
.buffer() .buffer()

View File

@ -42,17 +42,19 @@ enum GoToLineRowHighlights {}
impl GoToLine { impl GoToLine {
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) { fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
let handle = cx.view().downgrade(); let handle = cx.view().downgrade();
editor.register_action(move |_: &Toggle, cx| { editor
let Some(editor) = handle.upgrade() else { .register_action(move |_: &Toggle, cx| {
return; let Some(editor) = handle.upgrade() else {
}; return;
let Some(workspace) = editor.read(cx).workspace() else { };
return; let Some(workspace) = editor.read(cx).workspace() else {
}; return;
workspace.update(cx, |workspace, cx| { };
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
})
}) })
}); .detach();
} }
pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self { pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {

View File

@ -154,6 +154,14 @@ pub struct Subscription {
} }
impl Subscription { impl Subscription {
/// Creates a new subscription with a callback that gets invoked when
/// this subscription is dropped.
pub fn new(unsubscribe: impl 'static + FnOnce()) -> Self {
Self {
unsubscribe: Some(Box::new(unsubscribe)),
}
}
/// Detaches the subscription from this handle. The callback will /// Detaches the subscription from this handle. The callback will
/// continue to be invoked until the views or models it has been /// continue to be invoked until the views or models it has been
/// subscribed to are dropped /// subscribed to are dropped

View File

@ -294,7 +294,7 @@ impl MarkdownPreviewView {
let subscription = cx.subscribe(&editor, |this, editor, event: &EditorEvent, cx| { let subscription = cx.subscribe(&editor, |this, editor, event: &EditorEvent, cx| {
match event { match event {
EditorEvent::Edited => { EditorEvent::Edited { .. } => {
this.parse_markdown_from_active_editor(true, cx); this.parse_markdown_from_active_editor(true, cx);
} }
EditorEvent::SelectionsChanged { .. } => { EditorEvent::SelectionsChanged { .. } => {

View File

@ -789,6 +789,68 @@ impl MultiBuffer {
} }
} }
pub fn edited_ranges_for_transaction<D>(
&self,
transaction_id: TransactionId,
cx: &AppContext,
) -> Vec<Range<D>>
where
D: TextDimension + Ord + Sub<D, Output = D>,
{
if let Some(buffer) = self.as_singleton() {
return buffer
.read(cx)
.edited_ranges_for_transaction_id(transaction_id)
.collect::<Vec<_>>();
}
let Some(transaction) = self.history.transaction(transaction_id) else {
return Vec::new();
};
let mut ranges = Vec::new();
let snapshot = self.read(cx);
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>();
for (buffer_id, buffer_transaction) in &transaction.buffer_transactions {
let Some(buffer_state) = buffers.get(&buffer_id) else {
continue;
};
let buffer = buffer_state.buffer.read(cx);
for range in buffer.edited_ranges_for_transaction_id::<D>(*buffer_transaction) {
for excerpt_id in &buffer_state.excerpts {
cursor.seek(excerpt_id, Bias::Left, &());
if let Some(excerpt) = cursor.item() {
if excerpt.locator == *excerpt_id {
let excerpt_buffer_start =
excerpt.range.context.start.summary::<D>(buffer);
let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(buffer);
let excerpt_range = excerpt_buffer_start.clone()..excerpt_buffer_end;
if excerpt_range.contains(&range.start)
&& excerpt_range.contains(&range.end)
{
let excerpt_start = D::from_text_summary(&cursor.start().text);
let mut start = excerpt_start.clone();
start.add_assign(&(range.start - excerpt_buffer_start.clone()));
let mut end = excerpt_start;
end.add_assign(&(range.end - excerpt_buffer_start));
ranges.push(start..end);
break;
}
}
}
}
}
}
ranges.sort_by_key(|range| range.start.clone());
ranges
}
pub fn merge_transactions( pub fn merge_transactions(
&mut self, &mut self,
transaction: TransactionId, transaction: TransactionId,
@ -3968,6 +4030,17 @@ impl History {
} }
} }
fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> {
self.undo_stack
.iter()
.find(|transaction| transaction.id == transaction_id)
.or_else(|| {
self.redo_stack
.iter()
.find(|transaction| transaction.id == transaction_id)
})
}
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
self.undo_stack self.undo_stack
.iter_mut() .iter_mut()
@ -6060,6 +6133,15 @@ mod tests {
multibuffer.end_transaction_at(now, cx); multibuffer.end_transaction_at(now, cx);
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
// Verify edited ranges for transaction 1
assert_eq!(
multibuffer.edited_ranges_for_transaction(transaction_1, cx),
&[
Point::new(0, 0)..Point::new(0, 2),
Point::new(1, 0)..Point::new(1, 2)
]
);
// Edit buffer 1 through the multibuffer // Edit buffer 1 through the multibuffer
now += 2 * group_interval; now += 2 * group_interval;
multibuffer.start_transaction_at(now, cx); multibuffer.start_transaction_at(now, cx);

View File

@ -68,11 +68,13 @@ impl OutlineView {
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) { fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
if editor.mode() == EditorMode::Full { if editor.mode() == EditorMode::Full {
let handle = cx.view().downgrade(); let handle = cx.view().downgrade();
editor.register_action(move |action, cx| { editor
if let Some(editor) = handle.upgrade() { .register_action(move |action, cx| {
toggle(editor, action, cx); if let Some(editor) = handle.upgrade() {
} toggle(editor, action, cx);
}); }
})
.detach();
} }
} }

View File

@ -811,7 +811,7 @@ impl BufferSearchBar {
match event { match event {
editor::EditorEvent::Focused => self.query_editor_focused = true, editor::EditorEvent::Focused => self.query_editor_focused = true,
editor::EditorEvent::Blurred => self.query_editor_focused = false, editor::EditorEvent::Blurred => self.query_editor_focused = false,
editor::EditorEvent::Edited => { editor::EditorEvent::Edited { .. } => {
self.clear_matches(cx); self.clear_matches(cx);
let search = self.update_matches(cx); let search = self.update_matches(cx);

View File

@ -356,6 +356,19 @@ impl History {
} }
} }
fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> {
let entry = self
.undo_stack
.iter()
.rfind(|entry| entry.transaction.id == transaction_id)
.or_else(|| {
self.redo_stack
.iter()
.rfind(|entry| entry.transaction.id == transaction_id)
})?;
Some(&entry.transaction)
}
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
let entry = self let entry = self
.undo_stack .undo_stack
@ -1389,6 +1402,19 @@ impl Buffer {
self.history.finalize_last_transaction(); self.history.finalize_last_transaction();
} }
pub fn edited_ranges_for_transaction_id<D>(
&self,
transaction_id: TransactionId,
) -> impl '_ + Iterator<Item = Range<D>>
where
D: TextDimension,
{
self.history
.transaction(transaction_id)
.into_iter()
.flat_map(|transaction| self.edited_ranges_for_transaction(transaction))
}
pub fn edited_ranges_for_transaction<'a, D>( pub fn edited_ranges_for_transaction<'a, D>(
&'a self, &'a self,
transaction: &'a Transaction, transaction: &'a Transaction,

View File

@ -271,7 +271,9 @@ impl Vim {
EditorEvent::TransactionUndone { transaction_id } => Vim::update(cx, |vim, cx| { EditorEvent::TransactionUndone { transaction_id } => Vim::update(cx, |vim, cx| {
vim.transaction_undone(transaction_id, cx); vim.transaction_undone(transaction_id, cx);
}), }),
EditorEvent::Edited => Vim::update(cx, |vim, cx| vim.transaction_ended(editor, cx)), EditorEvent::Edited { .. } => {
Vim::update(cx, |vim, cx| vim.transaction_ended(editor, cx))
}
_ => {} _ => {}
})); }));

View File

@ -74,23 +74,30 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut ViewContex
editor.show_inline_completion(&Default::default(), cx); editor.show_inline_completion(&Default::default(), cx);
}, },
)) ))
.detach();
editor
.register_action(cx.listener( .register_action(cx.listener(
|editor, _: &copilot::NextSuggestion, cx: &mut ViewContext<Editor>| { |editor, _: &copilot::NextSuggestion, cx: &mut ViewContext<Editor>| {
editor.next_inline_completion(&Default::default(), cx); editor.next_inline_completion(&Default::default(), cx);
}, },
)) ))
.detach();
editor
.register_action(cx.listener( .register_action(cx.listener(
|editor, _: &copilot::PreviousSuggestion, cx: &mut ViewContext<Editor>| { |editor, _: &copilot::PreviousSuggestion, cx: &mut ViewContext<Editor>| {
editor.previous_inline_completion(&Default::default(), cx); editor.previous_inline_completion(&Default::default(), cx);
}, },
)) ))
.detach();
editor
.register_action(cx.listener( .register_action(cx.listener(
|editor, |editor,
_: &editor::actions::AcceptPartialCopilotSuggestion, _: &editor::actions::AcceptPartialCopilotSuggestion,
cx: &mut ViewContext<Editor>| { cx: &mut ViewContext<Editor>| {
editor.accept_partial_inline_completion(&Default::default(), cx); editor.accept_partial_inline_completion(&Default::default(), cx);
}, },
)); ))
.detach();
} }
fn assign_inline_completion_provider( fn assign_inline_completion_provider(