Add `ZED_SELECTION_CHANGE_CMD` to run a command on selection change

Michael Sloan created

Change summary

Cargo.lock                  |  2 +
crates/editor/Cargo.toml    |  1 
crates/editor/src/editor.rs | 43 ++++++++++++++++++++++++++++++++-----
crates/zed/Cargo.toml       |  1 
crates/zed/src/main.rs      | 44 +++++++++++++++++++++++++++++++++++++++
5 files changed, 85 insertions(+), 6 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5070,6 +5070,7 @@ dependencies = [
  "multi_buffer",
  "ordered-float 2.10.1",
  "parking_lot",
+ "postage",
  "pretty_assertions",
  "project",
  "rand 0.8.5",
@@ -20462,6 +20463,7 @@ dependencies = [
  "parking_lot",
  "paths",
  "picker",
+ "postage",
  "pretty_assertions",
  "profiling",
  "project",

crates/editor/Cargo.toml 🔗

@@ -92,6 +92,7 @@ uuid.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 workspace-hack.workspace = true
+postage.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/editor/src/editor.rs 🔗

@@ -176,17 +176,15 @@ use snippet::Snippet;
 use std::{
     any::TypeId,
     borrow::Cow,
-    cell::OnceCell,
-    cell::RefCell,
+    cell::{OnceCell, RefCell},
     cmp::{self, Ordering, Reverse},
     iter::Peekable,
     mem,
     num::NonZeroU32,
-    ops::Not,
-    ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
+    ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive},
     path::{Path, PathBuf},
     rc::Rc,
-    sync::Arc,
+    sync::{Arc, LazyLock},
     time::{Duration, Instant},
 };
 use sum_tree::TreeMap;
@@ -237,6 +235,21 @@ pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
 pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
 pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
 
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct LastCursorPosition {
+    pub path: PathBuf,
+    pub worktree_path: Arc<Path>,
+    pub point: Point,
+}
+
+pub static LAST_CURSOR_POSITION_WATCH: LazyLock<(
+    Mutex<postage::watch::Sender<Option<LastCursorPosition>>>,
+    postage::watch::Receiver<Option<LastCursorPosition>>,
+)> = LazyLock::new(|| {
+    let (sender, receiver) = postage::watch::channel();
+    (Mutex::new(sender), receiver)
+});
+
 pub type RenderDiffHunkControlsFn = Arc<
     dyn Fn(
         u32,
@@ -3018,10 +3031,28 @@ impl Editor {
         let new_cursor_position = newest_selection.head();
         let selection_start = newest_selection.start;
 
+        let new_cursor_point = new_cursor_position.to_point(buffer);
+        if let Some(project) = self.project()
+            && let Some((path, worktree_path)) =
+                self.file_at(new_cursor_point, cx).and_then(|file| {
+                    file.as_local().and_then(|file| {
+                        let worktree =
+                            project.read(cx).worktree_for_id(file.worktree_id(cx), cx)?;
+                        Some((file.abs_path(cx), worktree.read(cx).abs_path()))
+                    })
+                })
+        {
+            *LAST_CURSOR_POSITION_WATCH.0.lock().borrow_mut() = Some(LastCursorPosition {
+                path,
+                worktree_path,
+                point: new_cursor_point,
+            });
+        }
+
         if effects.nav_history.is_none() || effects.nav_history == Some(true) {
             self.push_to_nav_history(
                 *old_cursor_position,
-                Some(new_cursor_position.to_point(buffer)),
+                Some(new_cursor_point),
                 false,
                 effects.nav_history == Some(true),
                 cx,

crates/zed/Cargo.toml 🔗

@@ -164,6 +164,7 @@ zed_actions.workspace = true
 zeta.workspace = true
 zlog.workspace = true
 zlog_settings.workspace = true
+postage.workspace = true
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows.workspace = true

crates/zed/src/main.rs 🔗

@@ -17,6 +17,7 @@ use fs::{Fs, RealFs};
 use futures::{StreamExt, channel::oneshot, future};
 use git::GitHostingProviderRegistry;
 use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _};
+use postage::stream::Stream as _;
 
 use gpui_tokio::Tokio;
 use http_client::{Url, read_proxy_from_env};
@@ -747,6 +748,49 @@ pub fn main() {
             }
         })
         .detach();
+
+        if let Ok(selection_change_command) = env::var("ZED_SELECTION_CHANGE_CMD") {
+            log::info!(
+                "Will run {} when the selection changes",
+                selection_change_command
+            );
+            let mut cursor_reciever = editor::LAST_CURSOR_POSITION_WATCH.1.clone();
+            cx.background_spawn(async move {
+                while let Some(mut cursor) = cursor_reciever.recv().await {
+                    loop {
+                        // todo! Check if it's changed meanwhile and refresh.
+                        if let Some(cursor) = dbg!(&cursor) {
+                            let status = smol::process::Command::new(&selection_change_command)
+                                .arg(cursor.worktree_path.as_ref())
+                                .arg(format!(
+                                    "{}:{}:{}",
+                                    cursor.path.display(),
+                                    cursor.point.row + 1,
+                                    cursor.point.column + 1
+                                ))
+                                .status()
+                                .await;
+                            match status {
+                                Ok(status) => {
+                                    if !status.success() {
+                                        log::error!("Command failed with status {}", status);
+                                    }
+                                }
+                                Err(err) => {
+                                    log::error!("Command failed with error {}", err);
+                                }
+                            }
+                        }
+                        let new_cursor = cursor_reciever.borrow();
+                        if *new_cursor == cursor {
+                            break;
+                        }
+                        cursor = new_cursor.clone();
+                    }
+                }
+            })
+            .detach();
+        }
     });
 }