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..fe3a2c1a72 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -14,9 +14,10 @@ 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 _; use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; @@ -37,7 +38,7 @@ use std::{ env, io::{self, IsTerminal}, path::{Path, PathBuf}, - process, + process::{self, Stdio}, sync::Arc, }; use theme::{ @@ -732,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); @@ -747,6 +750,107 @@ 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_receiver = editor::LAST_CURSOR_POSITION_WATCH.1.clone(); + cx.background_spawn(async move { + // 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; + } + } + } + + // TODO: Could be more efficient + let Some(mut cursor) = cursor_receiver.borrow().clone() else { + continue; + }; + + 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(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 { + 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; + } + } + }) + .detach(); + } }); }