diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index a6efad6049..27d6f52c6c 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -79,7 +79,6 @@ pub fn init(cx: &mut AppContext) { workspace.toggle_panel_focus::(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, - ) { - 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) -> Option> { let workspace = self.workspace.upgrade()?; diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 0c050bfe53..463c8a6262 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -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, cx: &mut AppContext) { @@ -45,16 +46,11 @@ const PROMPT_HISTORY_MAX_LEN: usize = 20; pub struct InlineAssistant { next_assist_id: InlineAssistId, pending_assists: HashMap, - pending_assist_ids_by_editor: HashMap, EditorPendingAssists>, + pending_assist_ids_by_editor: HashMap, Vec>, prompt_history: VecDeque, telemetry: Option>, } -struct EditorPendingAssists { - window: AnyWindowHandle, - assist_ids: Vec, -} - 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, 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::(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, + 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::(*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, ) { match event { - EditorEvent::Edited => { + EditorEvent::Edited { .. } => { let prompt = self.prompt_editor.read(cx).text(cx); if self .prompt_history_ix diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 2361cbf4c8..c85320d909 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -592,19 +592,6 @@ impl PromptLibrary { } } - fn cancel_last_inline_assist( - &mut self, - _: &editor::actions::Cancel, - cx: &mut ViewContext, - ) { - 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)) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index d62f86419a..86283d0ed8 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -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(); } _ => {} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5339643f52..7ed7cf0e7c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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; @@ -512,7 +526,8 @@ pub struct Editor { gutter_dimensions: GutterDimensions, pub vim_replace_map: HashMap, String>, style: Option, - editor_actions: Vec)>>, + next_editor_action_id: EditorActionId, + editor_actions: Rc)>>>>, 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) { - 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) { + 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, ) -> Option { - 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( &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::(), 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::(), 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, }, BufferEdited, - Edited, + Edited { + transaction_id: clock::Lamport, + }, Reparsed, Focused, Blurred, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 395d8e33b6..3aef10b8e2 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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"), ] ); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7eca7685af..7be7564ce1 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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) } }); diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 09b2c6482b..1fe0804793 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -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::(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::(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 }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 5f637a7337..0d6391dc53 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -234,7 +234,7 @@ impl FollowableItem for Editor { fn to_follow_event(event: &EditorEvent) -> Option { match event { - EditorEvent::Edited => Some(FollowEvent::Unfollow), + EditorEvent::Edited { .. } => Some(FollowEvent::Unfollow), EditorEvent::SelectionsChanged { local } | EditorEvent::ScrollPositionChanged { local, .. } => { if *local { diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 785d56d334..93d29f043c 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -773,7 +773,7 @@ impl ExtensionsPage { event: &editor::EditorEvent, cx: &mut ViewContext, ) { - if let editor::EditorEvent::Edited = event { + if let editor::EditorEvent::Edited { .. } = event { self.query_contains_error = false; self.fetch_extensions_debounced(cx); } diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index 0d269fcdb5..d59dac0697 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -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() diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 1376cc7080..bce517997d 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -42,17 +42,19 @@ enum GoToLineRowHighlights {} impl GoToLine { fn register(editor: &mut Editor, cx: &mut ViewContext) { 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, cx: &mut ViewContext) -> Self { diff --git a/crates/gpui/src/subscription.rs b/crates/gpui/src/subscription.rs index c75d10ee62..700a248998 100644 --- a/crates/gpui/src/subscription.rs +++ b/crates/gpui/src/subscription.rs @@ -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 diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index e57e4da729..08851a4173 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -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 { .. } => { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 68514dfd3f..32bff5f12e 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -789,6 +789,68 @@ impl MultiBuffer { } } + pub fn edited_ranges_for_transaction( + &self, + transaction_id: TransactionId, + cx: &AppContext, + ) -> Vec> + where + D: TextDimension + Ord + Sub, + { + if let Some(buffer) = self.as_singleton() { + return buffer + .read(cx) + .edited_ranges_for_transaction_id(transaction_id) + .collect::>(); + } + + 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::(); + + 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::(*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::(buffer); + let excerpt_buffer_end = excerpt.range.context.end.summary::(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); diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index b3647361e6..f78d62f95f 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -68,11 +68,13 @@ impl OutlineView { fn register(editor: &mut Editor, cx: &mut ViewContext) { 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(); } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 37c49f34ed..c7e08e9597 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -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); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 58114734ac..a59d4785d4 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -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( + &self, + transaction_id: TransactionId, + ) -> impl '_ + Iterator> + 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, diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 78acb66e95..090cddfc3d 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -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)) + } _ => {} })); diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index 2cc6b23651..ff84297b58 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/inline_completion_registry.rs @@ -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.next_inline_completion(&Default::default(), cx); }, )) + .detach(); + editor .register_action(cx.listener( |editor, _: &copilot::PreviousSuggestion, cx: &mut ViewContext| { editor.previous_inline_completion(&Default::default(), cx); }, )) + .detach(); + editor .register_action(cx.listener( |editor, _: &editor::actions::AcceptPartialCopilotSuggestion, cx: &mut ViewContext| { editor.accept_partial_inline_completion(&Default::default(), cx); }, - )); + )) + .detach(); } fn assign_inline_completion_provider(