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);
})
.register_action(AssistantPanel::inline_assist)
.register_action(AssistantPanel::cancel_last_inline_assist)
.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>> {
let workspace = self.workspace.upgrade()?;

View File

@ -16,8 +16,8 @@ use editor::{
};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{
AnyWindowHandle, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight,
Global, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View,
AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View,
ViewContext, WeakView, WhiteSpace, WindowContext,
};
use language::{Buffer, Point, TransactionId};
@ -34,6 +34,7 @@ use std::{
};
use theme::ThemeSettings;
use ui::{prelude::*, Tooltip};
use util::RangeExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
pub fn init(telemetry: Arc<Telemetry>, cx: &mut AppContext) {
@ -45,16 +46,11 @@ const PROMPT_HISTORY_MAX_LEN: usize = 20;
pub struct InlineAssistant {
next_assist_id: InlineAssistId,
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>,
telemetry: Option<Arc<Telemetry>>,
}
struct EditorPendingAssists {
window: AnyWindowHandle,
assist_ids: Vec<InlineAssistId>,
}
impl Global for 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| {
Codegen::new(
editor.read(cx).buffer().clone(),
@ -116,7 +112,7 @@ impl InlineAssistant {
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
let prompt_editor = cx.new_view(|cx| {
InlineAssistEditor::new(
inline_assist_id,
assist_id,
gutter_dimensions.clone(),
self.prompt_history.clone(),
codegen.clone(),
@ -164,7 +160,7 @@ impl InlineAssistant {
});
self.pending_assists.insert(
inline_assist_id,
assist_id,
PendingInlineAssist {
include_context,
editor: editor.downgrade(),
@ -179,24 +175,35 @@ impl InlineAssistant {
_subscriptions: vec![
cx.subscribe(&prompt_editor, |inline_assist_editor, event, 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, {
let inline_assist_editor = prompt_editor.downgrade();
move |editor, event, cx| {
if let Some(inline_assist_editor) = inline_assist_editor.upgrade() {
if let EditorEvent::SelectionsChanged { local } = event {
if *local
&& inline_assist_editor
.focus_handle(cx)
.contains_focused(cx)
{
cx.focus_view(&editor);
}
}
}
}
editor.update(cx, |editor, _cx| {
editor.register_action(
move |_: &editor::actions::Newline, cx: &mut WindowContext| {
InlineAssistant::update_global(cx, |this, cx| {
this.handle_editor_action(assist_id, false, cx)
})
},
)
}),
editor.update(cx, |editor, _cx| {
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, {
let editor = editor.downgrade();
@ -204,19 +211,17 @@ impl InlineAssistant {
if let Some(editor) = editor.upgrade() {
InlineAssistant::update_global(cx, |this, 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| {
InlineAssistant::update_global(cx, |this, cx| match event {
CodegenEvent::Undone => {
this.finish_inline_assist(inline_assist_id, false, cx)
}
CodegenEvent::Undone => this.finish_inline_assist(assist_id, false, cx),
CodegenEvent::Finished => {
let pending_assist = if let Some(pending_assist) =
this.pending_assists.get(&inline_assist_id)
this.pending_assists.get(&assist_id)
{
pending_assist
} else {
@ -238,7 +243,7 @@ impl InlineAssistant {
let id = NotificationId::identified::<
InlineAssistantError,
>(
inline_assist_id.0
assist_id.0
);
workspace.show_toast(Toast::new(id, error), cx);
@ -248,7 +253,7 @@ impl InlineAssistant {
}
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
.entry(editor.downgrade())
.or_insert_with(|| EditorPendingAssists {
window: cx.window_handle(),
assist_ids: Vec::new(),
})
.assist_ids
.push(inline_assist_id);
.or_default()
.push(assist_id);
self.update_editor_highlights(editor, cx);
}
fn handle_inline_assistant_event(
fn handle_inline_assistant_editor_event(
&mut self,
inline_assist_editor: View<InlineAssistEditor>,
event: &InlineAssistEditorEvent,
@ -289,7 +290,7 @@ impl InlineAssistant {
self.finish_inline_assist(assist_id, true, cx);
}
InlineAssistEditorEvent::Dismissed => {
self.hide_inline_assist_decorations(assist_id, cx);
self.dismiss_inline_assist(assist_id, cx);
}
InlineAssistEditorEvent::Resized { height_in_lines } => {
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 {
for (editor, pending_assists) in &self.pending_assist_ids_by_editor {
if pending_assists.window == cx.window_handle() {
if let Some(editor) = editor.upgrade() {
if editor.read(cx).is_focused(cx) {
if let Some(assist_id) = pending_assists.assist_ids.last().copied() {
self.finish_inline_assist(assist_id, true, cx);
return true;
}
fn handle_editor_action(
&mut self,
assist_id: InlineAssistId,
undo: bool,
cx: &mut WindowContext,
) {
let Some(assist) = self.pending_assists.get(&assist_id) else {
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(
@ -319,15 +387,15 @@ impl InlineAssistant {
undo: bool,
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 hash_map::Entry::Occupied(mut entry) = self
.pending_assist_ids_by_editor
.entry(pending_assist.editor.clone())
{
entry.get_mut().assist_ids.retain(|id| *id != assist_id);
if entry.get().assist_ids.is_empty() {
entry.get_mut().retain(|id| *id != assist_id);
if entry.get().is_empty() {
entry.remove();
}
}
@ -344,11 +412,7 @@ impl InlineAssistant {
}
}
fn hide_inline_assist_decorations(
&mut self,
assist_id: InlineAssistId,
cx: &mut WindowContext,
) -> bool {
fn dismiss_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
return false;
};
@ -558,16 +622,14 @@ impl InlineAssistant {
let mut gutter_transformed_ranges = Vec::new();
let mut foreground_ranges = Vec::new();
let mut inserted_row_ranges = Vec::new();
let empty_inline_assist_ids = Vec::new();
let inline_assist_ids = self
let empty_assist_ids = Vec::new();
let assist_ids = self
.pending_assist_ids_by_editor
.get(&editor.downgrade())
.map_or(&empty_inline_assist_ids, |pending_assists| {
&pending_assists.assist_ids
});
.unwrap_or(&empty_assist_ids);
for inline_assist_id in inline_assist_ids {
if let Some(pending_assist) = self.pending_assists.get(inline_assist_id) {
for assist_id in assist_ids {
if let Some(pending_assist) = self.pending_assists.get(assist_id) {
let codegen = pending_assist.codegen.read(cx);
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
@ -1025,7 +1087,7 @@ impl InlineAssistEditor {
cx: &mut ViewContext<Self>,
) {
match event {
EditorEvent::Edited => {
EditorEvent::Edited { .. } => {
let prompt = self.prompt_editor.read(cx).text(cx);
if self
.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(
&mut self,
prompt_id: PromptId,
@ -743,7 +730,6 @@ impl PromptLibrary {
div()
.on_action(cx.listener(Self::focus_picker))
.on_action(cx.listener(Self::inline_assist))
.on_action(cx.listener(Self::cancel_last_inline_assist))
.flex_grow()
.h_full()
.pt(Spacing::XXLarge.rems(cx))

View File

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

View File

@ -116,15 +116,16 @@ use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use smallvec::SmallVec;
use snippet::Snippet;
use std::ops::Not as _;
use std::{
any::TypeId,
borrow::Cow,
cell::RefCell,
cmp::{self, Ordering, Reverse},
mem,
num::NonZeroU32,
ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
path::Path,
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
@ -377,6 +378,19 @@ impl Default for EditorStyle {
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 OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
@ -512,7 +526,8 @@ pub struct Editor {
gutter_dimensions: GutterDimensions,
pub vim_replace_map: HashMap<Range<usize>, String>,
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,
auto_replace_emoji_shortcode: bool,
show_git_blame_gutter: bool,
@ -1805,7 +1820,8 @@ impl Editor {
style: None,
show_cursor_names: false,
hovered_cursors: Default::default(),
editor_actions: Default::default(),
next_editor_action_id: EditorActionId::default(),
editor_actions: Rc::default(),
vim_replace_map: Default::default(),
show_inline_completions: mode == EditorMode::Full,
custom_context_menu: None,
@ -6448,29 +6464,9 @@ impl Editor {
return;
}
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
if let Some((selections, _)) = self.selection_history.transaction(tx_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);
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()
if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
if let Some((selections, _)) =
self.selection_history.transaction(transaction_id).cloned()
{
self.change_selections(None, cx, |s| {
s.select_anchors(selections.to_vec());
@ -6479,7 +6475,28 @@ impl Editor {
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(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,
cx: &mut ViewContext<Self>,
) -> Option<TransactionId> {
if let Some(tx_id) = self
if let Some(transaction_id) = self
.buffer
.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());
} else {
log::error!("unexpectedly ended a transaction that wasn't started by this editor");
}
cx.emit(EditorEvent::Edited);
Some(tx_id)
cx.emit(EditorEvent::Edited { transaction_id });
Some(transaction_id)
} else {
None
}
@ -11293,21 +11312,28 @@ impl Editor {
pub fn register_action<A: Action>(
&mut self,
listener: impl Fn(&A, &mut WindowContext) + 'static,
) -> &mut Self {
) -> Subscription {
let id = self.next_editor_action_id.post_inc();
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 _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
let editor_actions = self.editor_actions.clone();
Subscription::new(move || {
editor_actions.borrow_mut().remove(&id);
})
}
pub fn file_header_size(&self) -> u8 {
@ -11764,7 +11790,9 @@ pub enum EditorEvent {
ids: Vec<ExcerptId>,
},
BufferEdited,
Edited,
Edited {
transaction_id: clock::Lamport,
},
Reparsed,
Focused,
Blurred,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -154,6 +154,14 @@ pub struct 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
/// continue to be invoked until the views or models it has been
/// subscribed to are dropped

View File

@ -294,7 +294,7 @@ impl MarkdownPreviewView {
let subscription = cx.subscribe(&editor, |this, editor, event: &EditorEvent, cx| {
match event {
EditorEvent::Edited => {
EditorEvent::Edited { .. } => {
this.parse_markdown_from_active_editor(true, cx);
}
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(
&mut self,
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> {
self.undo_stack
.iter_mut()
@ -6060,6 +6133,15 @@ mod tests {
multibuffer.end_transaction_at(now, cx);
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
now += 2 * group_interval;
multibuffer.start_transaction_at(now, cx);

View File

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

View File

@ -811,7 +811,7 @@ impl BufferSearchBar {
match event {
editor::EditorEvent::Focused => self.query_editor_focused = true,
editor::EditorEvent::Blurred => self.query_editor_focused = false,
editor::EditorEvent::Edited => {
editor::EditorEvent::Edited { .. } => {
self.clear_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> {
let entry = self
.undo_stack
@ -1389,6 +1402,19 @@ impl Buffer {
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>(
&'a self,
transaction: &'a Transaction,

View File

@ -271,7 +271,9 @@ impl Vim {
EditorEvent::TransactionUndone { transaction_id } => Vim::update(cx, |vim, 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);
},
))
.detach();
editor
.register_action(cx.listener(
|editor, _: &copilot::NextSuggestion, cx: &mut ViewContext<Editor>| {
editor.next_inline_completion(&Default::default(), cx);
},
))
.detach();
editor
.register_action(cx.listener(
|editor, _: &copilot::PreviousSuggestion, cx: &mut ViewContext<Editor>| {
editor.previous_inline_completion(&Default::default(), cx);
},
))
.detach();
editor
.register_action(cx.listener(
|editor,
_: &editor::actions::AcceptPartialCopilotSuggestion,
cx: &mut ViewContext<Editor>| {
editor.accept_partial_inline_completion(&Default::default(), cx);
},
));
))
.detach();
}
fn assign_inline_completion_provider(