repl: Add clear output(s) command (#49631)

Kyle Kelley created

Closes #15947

This adds `repl:ClearCurrentOutput` and `repl:ClearOutputs` commands. No
keybindings are set for this. Just an action people can bind.

Release Notes:

- Added ability to clear outputs by action

Change summary

crates/repl/src/repl.rs             |  2 
crates/repl/src/repl_editor.rs      | 34 ++++++++++++++++++++++
crates/repl/src/repl_sessions_ui.rs |  2 +
crates/repl/src/session.rs          | 45 +++++++++++++++++++++++++++++++
4 files changed, 81 insertions(+), 2 deletions(-)

Detailed changes

crates/repl/src/repl.rs 🔗

@@ -20,7 +20,7 @@ pub use crate::jupyter_settings::JupyterSettings;
 pub use crate::kernels::{Kernel, KernelSpecification, KernelStatus, PythonEnvKernelSpecification};
 pub use crate::repl_editor::*;
 pub use crate::repl_sessions_ui::{
-    ClearOutputs, Interrupt, ReplSessionsPage, Restart, Run, Sessions, Shutdown,
+    ClearCurrentOutput, ClearOutputs, Interrupt, ReplSessionsPage, Restart, Run, Sessions, Shutdown,
 };
 pub use crate::repl_settings::ReplSettings;
 pub use crate::repl_store::ReplStore;

crates/repl/src/repl_editor.rs 🔗

@@ -14,7 +14,8 @@ use crate::kernels::PythonEnvKernelSpecification;
 use crate::repl_store::ReplStore;
 use crate::session::SessionEvent;
 use crate::{
-    ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart, Session, Shutdown,
+    ClearCurrentOutput, ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart,
+    Session, Shutdown,
 };
 
 pub fn assign_kernelspec(
@@ -349,6 +350,24 @@ pub fn clear_outputs(editor: WeakEntity<Editor>, cx: &mut App) {
     });
 }
 
+pub fn clear_current_output(editor: WeakEntity<Editor>, cx: &mut App) {
+    let Some(editor_entity) = editor.upgrade() else {
+        return;
+    };
+
+    let store = ReplStore::global(cx);
+    let entity_id = editor.entity_id();
+    let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
+        return;
+    };
+
+    let position = editor_entity.read(cx).selections.newest_anchor().head();
+
+    session.update(cx, |session, cx| {
+        session.clear_output_at_position(position, cx);
+    });
+}
+
 pub fn interrupt(editor: WeakEntity<Editor>, cx: &mut App) {
     let store = ReplStore::global(cx);
     let entity_id = editor.entity_id();
@@ -410,6 +429,19 @@ pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEnti
         })
         .detach();
 
+    editor
+        .register_action({
+            let editor_handle = editor_handle.clone();
+            move |_: &ClearCurrentOutput, _, cx| {
+                if !JupyterSettings::enabled(cx) {
+                    return;
+                }
+
+                crate::clear_current_output(editor_handle.clone(), cx);
+            }
+        })
+        .detach();
+
     editor
         .register_action({
             let editor_handle = editor_handle.clone();

crates/repl/src/repl_sessions_ui.rs 🔗

@@ -21,6 +21,8 @@ actions!(
         RunInPlace,
         /// Clears all outputs in the REPL.
         ClearOutputs,
+        /// Clears the output of the cell at the current cursor position.
+        ClearCurrentOutput,
         /// Opens the REPL sessions panel.
         Sessions,
         /// Interrupts the currently running kernel.

crates/repl/src/session.rs 🔗

@@ -514,6 +514,51 @@ impl Session {
         self.result_inlays.clear();
     }
 
+    pub fn clear_output_at_position(&mut self, position: Anchor, cx: &mut Context<Self>) {
+        let Some(editor) = self.editor.upgrade() else {
+            return;
+        };
+
+        let (block_id, code_range, msg_id) = {
+            let snapshot = editor.read(cx).buffer().read(cx).read(cx);
+            let pos_range = position..position;
+
+            let block_to_remove = self
+                .blocks
+                .iter()
+                .find(|(_, block)| block.code_range.includes(&pos_range, &snapshot));
+
+            let Some((msg_id, block)) = block_to_remove else {
+                return;
+            };
+
+            (block.block_id, block.code_range.clone(), msg_id.clone())
+        };
+
+        let inlay_to_remove = self.result_inlays.get(&msg_id).map(|(id, _, _)| *id);
+
+        self.blocks.remove(&msg_id);
+        if inlay_to_remove.is_some() {
+            self.result_inlays.remove(&msg_id);
+        }
+
+        self.editor
+            .update(cx, |editor, cx| {
+                let mut block_ids = HashSet::default();
+                block_ids.insert(block_id);
+                editor.remove_blocks(block_ids, None, cx);
+
+                if let Some(inlay_id) = inlay_to_remove {
+                    editor.splice_inlays(&[inlay_id], vec![], cx);
+                }
+
+                editor.remove_gutter_highlights::<ReplExecutedRange>(vec![code_range], cx);
+            })
+            .ok();
+
+        cx.notify();
+    }
+
     pub fn execute(
         &mut self,
         code: String,