diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 987d9e29eb..a44c7d0853 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -38,6 +38,7 @@ mod proposed_changes_editor; mod rust_analyzer_ext; pub mod scroll; mod selections_collection; +mod smooth_cursor_manager; pub mod tasks; #[cfg(test)] @@ -152,6 +153,7 @@ use selections_collection::{ use serde::{Deserialize, Serialize}; use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore}; use smallvec::SmallVec; +use smooth_cursor_manager::SmoothCursorManager; use snippet::Snippet; use std::{ any::TypeId, @@ -760,6 +762,7 @@ pub struct Editor { toggle_fold_multiple_buffers: Task<()>, _scroll_cursor_center_top_bottom_task: Task<()>, serialize_selections: Task<()>, + smooth_cursor_manager: SmoothCursorManager, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -1467,6 +1470,7 @@ impl Editor { serialize_selections: Task::ready(()), text_style_refinement: None, load_diff_task: load_uncommitted_diff, + smooth_cursor_manager: SmoothCursorManager::Inactive, }; this.tasks_update_task = Some(this.refresh_runnables(window, cx)); this._subscriptions.extend(project_subscriptions); @@ -2030,6 +2034,7 @@ impl Editor { local: bool, old_cursor_position: &Anchor, show_completions: bool, + pre_edit_pixel_points: HashMap>>, window: &mut Window, cx: &mut Context, ) { @@ -2162,6 +2167,23 @@ impl Editor { hide_hover(self, cx); + let mut post_edit_pixel_points = HashMap::default(); + + for selection in self.selections.disjoint_anchors().iter() { + let head_point = + self.to_pixel_point(selection.head(), &self.snapshot(window, cx), window); + post_edit_pixel_points.insert(selection.id, head_point); + } + + if let Some(pending) = self.selections.pending_anchor() { + let head_point = + self.to_pixel_point(pending.head(), &self.snapshot(window, cx), window); + post_edit_pixel_points.insert(pending.id, head_point); + } + + self.smooth_cursor_manager + .update(pre_edit_pixel_points, post_edit_pixel_points); + if old_cursor_position.to_display_point(&display_map).row() != new_cursor_position.to_display_point(&display_map).row() { @@ -2279,6 +2301,21 @@ impl Editor { change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, ) -> R { let old_cursor_position = self.selections.newest_anchor().head(); + + let mut pre_edit_pixel_points = HashMap::default(); + + for selection in self.selections.disjoint_anchors().iter() { + let head_point = + self.to_pixel_point(selection.head(), &self.snapshot(window, cx), window); + pre_edit_pixel_points.insert(selection.id, head_point); + } + + if let Some(pending) = self.selections.pending_anchor() { + let head_point = + self.to_pixel_point(pending.head(), &self.snapshot(window, cx), window); + pre_edit_pixel_points.insert(pending.id, head_point); + } + self.push_to_selection_history(); let (changed, result) = self.selections.change_with(cx, change); @@ -2287,7 +2324,14 @@ impl Editor { if let Some(autoscroll) = autoscroll { self.request_autoscroll(autoscroll, cx); } - self.selections_did_change(true, &old_cursor_position, request_completions, window, cx); + self.selections_did_change( + true, + &old_cursor_position, + request_completions, + pre_edit_pixel_points, + window, + cx, + ); if self.should_open_signature_help_automatically( &old_cursor_position, @@ -3100,6 +3144,20 @@ impl Editor { let initial_buffer_versions = jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx); + let mut pre_edit_pixel_points = HashMap::default(); + + for selection in this.selections.disjoint_anchors().iter() { + let head_point = + this.to_pixel_point(selection.head(), &this.snapshot(window, cx), window); + pre_edit_pixel_points.insert(selection.id, head_point); + } + + if let Some(pending) = this.selections.pending_anchor() { + let head_point = + this.to_pixel_point(pending.head(), &this.snapshot(window, cx), window); + pre_edit_pixel_points.insert(pending.id, head_point); + } + this.buffer.update(cx, |buffer, cx| { buffer.edit(edits, this.autoindent_mode.clone(), cx); }); @@ -3201,6 +3259,22 @@ impl Editor { linked_editing_ranges::refresh_linked_ranges(this, window, cx); this.refresh_inline_completion(true, false, window, cx); jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); + + let mut post_edit_pixel_points = HashMap::default(); + + for selection in this.selections.disjoint_anchors().iter() { + let head_point = + this.to_pixel_point(selection.head(), &this.snapshot(window, cx), window); + post_edit_pixel_points.insert(selection.id, head_point); + } + + if let Some(pending) = this.selections.pending_anchor() { + let head_point = + this.to_pixel_point(pending.head(), &this.snapshot(window, cx), window); + post_edit_pixel_points.insert(pending.id, head_point); + } + this.smooth_cursor_manager + .update(pre_edit_pixel_points, post_edit_pixel_points); }); } @@ -3253,6 +3327,20 @@ impl Editor { pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context) { self.transact(window, cx, |this, window, cx| { + let mut pre_edit_pixel_points = HashMap::default(); + + for selection in this.selections.disjoint_anchors().iter() { + let head_point = + this.to_pixel_point(selection.head(), &this.snapshot(window, cx), window); + pre_edit_pixel_points.insert(selection.id, head_point); + } + + if let Some(pending) = this.selections.pending_anchor() { + let head_point = + this.to_pixel_point(pending.head(), &this.snapshot(window, cx), window); + pre_edit_pixel_points.insert(pending.id, head_point); + } + let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { let selections = this.selections.all::(cx); let multi_buffer = this.buffer.read(cx); @@ -3363,6 +3451,23 @@ impl Editor { s.select(new_selections) }); this.refresh_inline_completion(true, false, window, cx); + + let mut post_edit_pixel_points = HashMap::default(); + + for selection in this.selections.disjoint_anchors().iter() { + let head_point = + this.to_pixel_point(selection.head(), &this.snapshot(window, cx), window); + post_edit_pixel_points.insert(selection.id, head_point); + } + + if let Some(pending) = this.selections.pending_anchor() { + let head_point = + this.to_pixel_point(pending.head(), &this.snapshot(window, cx), window); + post_edit_pixel_points.insert(pending.id, head_point); + } + + this.smooth_cursor_manager + .update(pre_edit_pixel_points, post_edit_pixel_points); }); } @@ -13185,7 +13290,14 @@ impl Editor { s.clear_pending(); } }); - self.selections_did_change(false, &old_cursor_position, true, window, cx); + self.selections_did_change( + false, + &old_cursor_position, + true, + HashMap::default(), + window, + cx, + ); } fn push_to_selection_history(&mut self) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3095f77862..782db9b4f0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -21,9 +21,9 @@ use crate::{ EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock, GoToHunk, GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, OpenExcerpts, PageDown, PageUp, - Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, - StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, - FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, + Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SmoothCursorManager, + SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, + CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; @@ -83,6 +83,7 @@ const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; const MIN_SCROLL_THUMB_SIZE: f32 = 25.; struct SelectionLayout { + id: usize, head: DisplayPoint, cursor_shape: CursorShape, is_newest: bool, @@ -140,6 +141,7 @@ impl SelectionLayout { } Self { + id: selection.id, head, cursor_shape, is_newest, @@ -1151,12 +1153,29 @@ impl EditorElement { let cursor_layouts = self.editor.update(cx, |editor, cx| { let mut cursors = Vec::new(); + let is_animating = + !matches!(editor.smooth_cursor_manager, SmoothCursorManager::Inactive); + let animated_selection_ids = if is_animating { + match &editor.smooth_cursor_manager { + SmoothCursorManager::Active { cursors } => { + cursors.keys().copied().collect::>() + } + _ => HashSet::default(), + } + } else { + HashSet::default() + }; + let show_local_cursors = editor.show_local_cursors(window, cx); for (player_color, selections) in selections { for selection in selections { let cursor_position = selection.head; + if animated_selection_ids.contains(&selection.id) { + continue; + } + let in_range = visible_display_row_range.contains(&cursor_position.row()); if (selection.is_local && !show_local_cursors) || !in_range @@ -1283,6 +1302,19 @@ impl EditorElement { } } + if is_animating { + let animated_cursors = self.layout_animated_cursors( + editor, + content_origin, + line_height, + em_advance, + selections, + window, + cx, + ); + cursors.extend(animated_cursors); + } + cursors }); @@ -1293,6 +1325,47 @@ impl EditorElement { cursor_layouts } + fn layout_animated_cursors( + &self, + editor: &mut Editor, + content_origin: gpui::Point, + line_height: Pixels, + em_advance: Pixels, + selections: &[(PlayerColor, Vec)], + window: &mut Window, + cx: &mut App, + ) -> Vec { + let new_positions = editor.smooth_cursor_manager.animate(); + if !new_positions.is_empty() { + window.request_animation_frame(); + } + new_positions + .into_iter() + .map(|(id, position)| { + // todo smit: worst way to get cursor shape and player color + let (cursor_shape, player_color) = selections + .iter() + .find_map(|(player_color, sels)| { + sels.iter() + .find(|sel| sel.id == id) + .map(|sel| (sel.cursor_shape, *player_color)) + }) + .unwrap_or((CursorShape::Bar, editor.current_user_player_color(cx))); + let mut cursor = CursorLayout { + color: player_color.cursor, + block_width: em_advance, + origin: position, + line_height, + shape: cursor_shape, + block_text: None, + cursor_name: None, + }; + cursor.layout(content_origin, None, window, cx); + cursor + }) + .collect() + } + fn layout_scrollbars( &self, snapshot: &EditorSnapshot, diff --git a/crates/editor/src/smooth_cursor_manager.rs b/crates/editor/src/smooth_cursor_manager.rs new file mode 100644 index 0000000000..71f985dd82 --- /dev/null +++ b/crates/editor/src/smooth_cursor_manager.rs @@ -0,0 +1,117 @@ +use collections::HashMap; +use gpui::Pixels; + +const DELTA_PERCENT_PER_FRAME: f32 = 0.01; + +pub struct Cursor { + current_position: gpui::Point, + target_position: gpui::Point, +} + +pub enum SmoothCursorManager { + Inactive, + Active { cursors: HashMap }, +} + +impl SmoothCursorManager { + pub fn update( + &mut self, + source_positions: HashMap>>, + target_positions: HashMap>>, + ) { + if source_positions.len() == 1 && target_positions.len() == 1 { + let old_id = source_positions.keys().next().unwrap(); + let new_id = target_positions.keys().next().unwrap(); + if old_id != new_id { + if let (Some(Some(old_pos)), Some(Some(new_pos))) = ( + source_positions.values().next(), + target_positions.values().next(), + ) { + *self = Self::Active { + cursors: HashMap::from_iter([( + *new_id, + Cursor { + current_position: *old_pos, + target_position: *new_pos, + }, + )]), + }; + return; + } + } + } + match self { + Self::Inactive => { + let mut cursors = HashMap::default(); + for (id, target_position) in target_positions.iter() { + let Some(target_position) = target_position else { + continue; + }; + let Some(Some(source_position)) = source_positions.get(id) else { + continue; + }; + if source_position == target_position { + continue; + } + cursors.insert( + *id, + Cursor { + current_position: *source_position, + target_position: *target_position, + }, + ); + } + if !cursors.is_empty() { + *self = Self::Active { cursors }; + } + } + Self::Active { cursors } => { + for (id, target_position) in target_positions.iter() { + let Some(target_position) = target_position else { + continue; + }; + if let Some(cursor) = cursors.get_mut(id) { + cursor.target_position = *target_position; + } + } + } + } + } + + pub fn animate(&mut self) -> HashMap> { + match self { + Self::Inactive => HashMap::default(), + Self::Active { cursors } => { + let mut new_positions = HashMap::default(); + let mut completed = Vec::new(); + + for (id, cursor) in cursors.iter_mut() { + let dx = cursor.target_position.x - cursor.current_position.x; + let dy = cursor.target_position.y - cursor.current_position.y; + + let distance = (dx.0.powi(2) + dy.0.powi(2)).sqrt(); + if distance < 0.2 { + new_positions.insert(*id, cursor.target_position); + completed.push(*id); + } else { + cursor.current_position.x = + Pixels(cursor.current_position.x.0 + dx.0 * DELTA_PERCENT_PER_FRAME); + cursor.current_position.y = + Pixels(cursor.current_position.y.0 + dy.0 * DELTA_PERCENT_PER_FRAME); + new_positions.insert(*id, cursor.current_position); + } + } + + for id in completed { + cursors.remove(&id); + } + + if cursors.is_empty() { + *self = Self::Inactive; + } + + new_positions + } + } + } +}