From 2755cd8ec73151675d2f29374ce8cf3b1753e999 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 19 Aug 2025 14:46:16 -0600 Subject: [PATCH 1/4] Add `ZED_SELECTION_CHANGE_CMD` to run a command on selection change --- 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(-) diff --git a/Cargo.lock b/Cargo.lock index 4a5dec4734..4178d91c71 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 339f98ae8b..7c8fe56475 100644 --- a/crates/editor/Cargo.toml +++ b/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 diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7c36a41046..05688ee6ee 100644 --- a/crates/editor/src/editor.rs +++ b/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, + pub point: Point, +} + +pub static LAST_CURSOR_POSITION_WATCH: LazyLock<( + Mutex>>, + postage::watch::Receiver>, +)> = 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, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d69efaf6c0..3634fbb692 100644 --- a/crates/zed/Cargo.toml +++ b/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 diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index df30d4dd7b..374445cbaf 100644 --- a/crates/zed/src/main.rs +++ b/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(); + } }); } From 11ad0b57937955cb7089296fcffe52171d96897b Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 19 Aug 2025 15:47:14 -0600 Subject: [PATCH 2/4] Rerun command if it is a file and the file changes --- crates/zed/src/main.rs | 105 ++++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 28 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 374445cbaf..e22df2efb9 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -14,7 +14,7 @@ use editor::Editor; use extension::ExtensionHostProxy; use extension_host::ExtensionStore; use fs::{Fs, RealFs}; -use futures::{StreamExt, channel::oneshot, future}; +use futures::{FutureExt, StreamExt, channel::oneshot, future, select_biased}; use git::GitHostingProviderRegistry; use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; use postage::stream::Stream as _; @@ -733,6 +733,8 @@ pub fn main() { } } + // todo! cleanup + let fs = app_state.fs.clone(); let app_state = app_state.clone(); crate::zed::component_preview::init(app_state.clone(), cx); @@ -754,38 +756,85 @@ pub fn main() { "Will run {} when the selection changes", selection_change_command ); - let mut cursor_reciever = editor::LAST_CURSOR_POSITION_WATCH.1.clone(); + + let mut cursor_receiver = 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); - } + // Set up file watcher for the command file + let command_path = PathBuf::from(&selection_change_command); + let mut file_changes = if command_path.exists() { + let (events, _) = fs + .watch(&command_path, std::time::Duration::from_millis(100)) + .await; + Some(events) + } else { + log::warn!( + "Command file {} does not exist, only watching selection changes", + command_path.display() + ); + None + }; + + loop { + select_biased! { + // Handle cursor position changes + cursor_update = cursor_receiver.recv().fuse() => { + if cursor_update.is_none() { + // Cursor watcher ended + log::warn!("Cursor watcher for {} ended", command_path.display()); + break; + } + }, + + // Handle file changes to the command file + file_change = async { + if let Some(ref mut events) = file_changes { + events.next().await + } else { + future::pending().await + } + }.fuse() => { + if file_change.is_none() { + // File watcher ended + log::warn!("File watcher for {} ended", command_path.display()); + file_changes = None; } } - let new_cursor = cursor_reciever.borrow(); - if *new_cursor == cursor { + } + + // TODO: Could be more efficient + let Some(mut cursor) = cursor_receiver.borrow().clone() else { + continue; + }; + + loop { + 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 Some(new_cursor) = cursor_receiver.borrow().clone() else { + break; + }; + if new_cursor == cursor { break; } - cursor = new_cursor.clone(); + cursor = new_cursor; } } }) From 2d20b5d8503cada1b23752803701c3b4d943b142 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 19 Aug 2025 15:53:09 -0600 Subject: [PATCH 3/4] Log command that is run --- crates/zed/src/main.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e22df2efb9..04325b22e5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -807,14 +807,21 @@ pub fn main() { }; loop { + let cursor_position_arg = format!( + "{}:{}:{}", + cursor.path.display(), + cursor.point.row + 1, + cursor.point.column + 1 + ); + log::info!( + "Running {} {} {}", + selection_change_command, + cursor.worktree_path.display(), + cursor_position_arg + ); 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 - )) + .arg(cursor_position_arg) .status() .await; match status { From a1a6031c6a4eb5d0e6b7b86264f565fd8f0a8807 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 19 Aug 2025 16:08:12 -0600 Subject: [PATCH 4/4] Close stdin --- crates/zed/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 04325b22e5..fe3a2c1a72 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -38,7 +38,7 @@ use std::{ env, io::{self, IsTerminal}, path::{Path, PathBuf}, - process, + process::{self, Stdio}, sync::Arc, }; use theme::{ @@ -822,6 +822,10 @@ pub fn main() { let status = smol::process::Command::new(&selection_change_command) .arg(cursor.worktree_path.as_ref()) .arg(cursor_position_arg) + .stdin(Stdio::null()) + // todo! It'd be better to distinguish the output in logs. + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) .status() .await; match status {