Invalidate anchors when they get deleted (#14116)

Kyle Kelley and Antonio created

Allows deleting the outputs directly within the editor. This also fixes
the overlap logic to make sure that the ends and the starts are
compared.


https://github.com/zed-industries/zed/assets/836375/84f5f582-95f3-4c6a-a3c9-54da6009e34d

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>

Change summary

Cargo.lock                        |  1 
crates/multi_buffer/src/anchor.rs |  6 --
crates/repl/Cargo.toml            |  1 
crates/repl/src/session.rs        | 73 +++++++++++++++++++++++++++++++-
4 files changed, 72 insertions(+), 9 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8664,6 +8664,7 @@ dependencies = [
  "image",
  "language",
  "log",
+ "multi_buffer",
  "project",
  "runtimelib",
  "schemars",

crates/multi_buffer/src/anchor.rs 🔗

@@ -131,11 +131,7 @@ impl AnchorRangeExt for Range<Anchor> {
     }
 
     fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
-        let start_cmp = self.start.cmp(&other.start, buffer);
-        let end_cmp = self.end.cmp(&other.end, buffer);
-
-        (start_cmp == Ordering::Less || start_cmp == Ordering::Equal)
-            && (end_cmp == Ordering::Greater || end_cmp == Ordering::Equal)
+        self.end.cmp(&other.start, buffer).is_ge() && self.start.cmp(&other.end, buffer).is_le()
     }
 
     fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {

crates/repl/Cargo.toml 🔗

@@ -24,6 +24,7 @@ futures.workspace = true
 image.workspace = true
 language.workspace = true
 log.workspace = true
+multi_buffer.workspace = true
 project.workspace = true
 runtimelib.workspace = true
 schemars.workspace = true

crates/repl/src/session.rs 🔗

@@ -7,10 +7,13 @@ use editor::{
     display_map::{
         BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
     },
-    Anchor, AnchorRangeExt as _, Editor,
+    Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint,
 };
 use futures::{FutureExt as _, StreamExt as _};
-use gpui::{div, prelude::*, EventEmitter, Render, Task, View, ViewContext, WeakView};
+use gpui::{
+    div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView,
+};
+use language::Point;
 use project::Fs;
 use runtimelib::{
     ExecuteRequest, InterruptRequest, JupyterMessage, JupyterMessageContent, KernelInfoRequest,
@@ -27,11 +30,13 @@ pub struct Session {
     blocks: HashMap<String, EditorBlock>,
     pub messaging_task: Task<()>,
     pub kernel_specification: KernelSpecification,
+    _buffer_subscription: Subscription,
 }
 
 struct EditorBlock {
     editor: WeakView<Editor>,
     code_range: Range<Anchor>,
+    invalidation_anchor: Anchor,
     block_id: BlockId,
     execution_view: View<ExecutionView>,
 }
@@ -45,7 +50,25 @@ impl EditorBlock {
     ) -> anyhow::Result<Self> {
         let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx));
 
-        let block_id = editor.update(cx, |editor, cx| {
+        let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| {
+            let buffer = editor.buffer().clone();
+            let buffer_snapshot = buffer.read(cx).snapshot(cx);
+            let end_point = code_range.end.to_point(&buffer_snapshot);
+            let next_row_start = end_point + Point::new(1, 0);
+            if next_row_start > buffer_snapshot.max_point() {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.edit(
+                        [(
+                            buffer_snapshot.max_point()..buffer_snapshot.max_point(),
+                            "\n",
+                        )],
+                        None,
+                        cx,
+                    )
+                });
+            }
+
+            let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start);
             let block = BlockProperties {
                 position: code_range.end,
                 height: execution_view.num_lines(cx).saturating_add(1),
@@ -54,12 +77,14 @@ impl EditorBlock {
                 disposition: BlockDisposition::Below,
             };
 
-            editor.insert_blocks([block], None, cx)[0]
+            let block_id = editor.insert_blocks([block], None, cx)[0];
+            (block_id, invalidation_anchor)
         })?;
 
         anyhow::Ok(Self {
             editor,
             code_range,
+            invalidation_anchor,
             block_id,
             execution_view,
         })
@@ -179,15 +204,55 @@ impl Session {
             })
             .shared();
 
+        let subscription = match editor.upgrade() {
+            Some(editor) => {
+                let buffer = editor.read(cx).buffer().clone();
+                cx.subscribe(&buffer, Self::on_buffer_event)
+            }
+            None => Subscription::new(|| {}),
+        };
+
         return Self {
             editor,
             kernel: Kernel::StartingKernel(pending_kernel),
             messaging_task: Task::ready(()),
             blocks: HashMap::default(),
             kernel_specification,
+            _buffer_subscription: subscription,
         };
     }
 
+    fn on_buffer_event(
+        &mut self,
+        buffer: Model<MultiBuffer>,
+        event: &multi_buffer::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let multi_buffer::Event::Edited { .. } = event {
+            let snapshot = buffer.read(cx).snapshot(cx);
+
+            let mut blocks_to_remove: HashSet<BlockId> = HashSet::default();
+
+            self.blocks.retain(|_id, block| {
+                if block.invalidation_anchor.is_valid(&snapshot) {
+                    true
+                } else {
+                    blocks_to_remove.insert(block.block_id);
+                    false
+                }
+            });
+
+            if !blocks_to_remove.is_empty() {
+                self.editor
+                    .update(cx, |editor, cx| {
+                        editor.remove_blocks(blocks_to_remove, None, cx);
+                    })
+                    .ok();
+                cx.notify();
+            }
+        }
+    }
+
     fn send(&mut self, message: JupyterMessage, _cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
         match &mut self.kernel {
             Kernel::RunningKernel(kernel) => {