Merge branch 'main' into multibuffer-following

Max Brunsfeld created

Change summary

crates/drag_and_drop/src/drag_and_drop.rs |  4 -
crates/editor/src/editor.rs               |  1 
crates/editor/src/editor_tests.rs         | 26 ++++++++++++
crates/editor/src/items.rs                |  7 ++
crates/editor/src/scroll.rs               |  8 +-
crates/editor/src/scroll/actions.rs       |  6 +-
crates/workspace/src/persistence.rs       | 51 +++++++++++++++---------
crates/workspace/src/persistence/model.rs |  8 ++
crates/workspace/src/workspace.rs         | 34 +++++++++++++---
crates/zed/src/main.rs                    | 14 +++++
10 files changed, 116 insertions(+), 43 deletions(-)

Detailed changes

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -139,9 +139,7 @@ impl<V: View> DragAndDrop<V> {
                     region_offset,
                     region,
                 }) => {
-                    if (dbg!(event.position) - (dbg!(region.origin() + region_offset))).length()
-                        > DEAD_ZONE
-                    {
+                    if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
                         this.currently_dragged = Some(State::Dragging {
                             window_id,
                             region_offset,

crates/editor/src/editor_tests.rs 🔗

@@ -4991,9 +4991,11 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
         )
     });
 
+    let is_still_following = Rc::new(RefCell::new(true));
     let pending_update = Rc::new(RefCell::new(None));
     follower.update(cx, {
         let update = pending_update.clone();
+        let is_still_following = is_still_following.clone();
         |_, cx| {
             cx.subscribe(&leader, move |_, leader, event, cx| {
                 leader
@@ -5001,6 +5003,13 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
                     .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
             })
             .detach();
+
+            cx.subscribe(&follower, move |_, _, event, cx| {
+                if Editor::should_unfollow_on_event(event, cx) {
+                    *is_still_following.borrow_mut() = false;
+                }
+            })
+            .detach();
         }
     });
 
@@ -5017,6 +5026,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
     follower.read_with(cx, |follower, cx| {
         assert_eq!(follower.selections.ranges(cx), vec![1..1]);
     });
+    assert_eq!(*is_still_following.borrow(), true);
 
     // Update the scroll position only
     leader.update(cx, |leader, cx| {
@@ -5032,6 +5042,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
         follower.update(cx, |follower, cx| follower.scroll_position(cx)),
         vec2f(1.5, 3.5)
     );
+    assert_eq!(*is_still_following.borrow(), true);
 
     // Update the selections and scroll position. The follower's scroll position is updated
     // via autoscroll, not via the leader's exact scroll position.
@@ -5050,6 +5061,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
         assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0));
         assert_eq!(follower.selections.ranges(cx), vec![0..0]);
     });
+    assert_eq!(*is_still_following.borrow(), true);
 
     // Creating a pending selection that precedes another selection
     leader.update(cx, |leader, cx| {
@@ -5065,6 +5077,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
     follower.read_with(cx, |follower, cx| {
         assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
     });
+    assert_eq!(*is_still_following.borrow(), true);
 
     // Extend the pending selection so that it surrounds another selection
     leader.update(cx, |leader, cx| {
@@ -5079,6 +5092,19 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
     follower.read_with(cx, |follower, cx| {
         assert_eq!(follower.selections.ranges(cx), vec![0..2]);
     });
+
+    // Scrolling locally breaks the follow
+    follower.update(cx, |follower, cx| {
+        let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
+        follower.set_scroll_anchor(
+            ScrollAnchor {
+                top_anchor,
+                offset: vec2f(0.0, 0.5),
+            },
+            cx,
+        );
+    });
+    assert_eq!(*is_still_following.borrow(), false);
 }
 
 #[gpui::test]

crates/editor/src/items.rs 🔗

@@ -138,7 +138,7 @@ impl FollowableItem for Editor {
                 }
 
                 if let Some(scroll_top_anchor) = scroll_top_anchor {
-                    editor.set_scroll_anchor(
+                    editor.set_scroll_anchor_remote(
                         ScrollAnchor {
                             top_anchor: scroll_top_anchor,
                             offset: vec2f(state.scroll_x, state.scroll_y),
@@ -363,7 +363,10 @@ impl FollowableItem for Editor {
                     this.set_selections_from_remote(selections, cx);
                     this.request_autoscroll_remotely(Autoscroll::newest(), cx);
                 } else if let Some(anchor) = scroll_top_anchor {
-                    this.set_scroll_anchor(ScrollAnchor {top_anchor: anchor, offset: vec2f(message.scroll_x, message.scroll_y) }, cx);
+                    this.set_scroll_anchor_remote(ScrollAnchor {
+                        top_anchor: anchor,
+                        offset: vec2f(message.scroll_x, message.scroll_y)
+                    }, cx);
                 }
             });
             Ok(())

crates/editor/src/scroll.rs 🔗

@@ -284,17 +284,17 @@ impl Editor {
     }
 
     pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
-        self.set_scroll_anchor_internal(scroll_anchor, true, cx);
+        hide_hover(self, cx);
+        self.scroll_manager.set_anchor(scroll_anchor, true, cx);
     }
 
-    pub(crate) fn set_scroll_anchor_internal(
+    pub(crate) fn set_scroll_anchor_remote(
         &mut self,
         scroll_anchor: ScrollAnchor,
-        local: bool,
         cx: &mut ViewContext<Self>,
     ) {
         hide_hover(self, cx);
-        self.scroll_manager.set_anchor(scroll_anchor, local, cx);
+        self.scroll_manager.set_anchor(scroll_anchor, false, cx);
     }
 
     pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {

crates/editor/src/scroll/actions.rs 🔗

@@ -64,15 +64,15 @@ impl Editor {
             return None;
         }
 
-        self.context_menu.as_mut()?;
+        if self.mouse_context_menu.read(cx).visible() {
+            return None;
+        }
 
         if matches!(self.mode, EditorMode::SingleLine) {
             cx.propagate_action();
             return None;
         }
-
         self.request_autoscroll(Autoscroll::Next, cx);
-
         Some(())
     }
 

crates/workspace/src/persistence.rs 🔗

@@ -8,7 +8,7 @@ use anyhow::{anyhow, bail, Context, Result};
 use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
 use gpui::Axis;
 
-use util::{iife, unzip_option, ResultExt};
+use util::{ unzip_option, ResultExt};
 
 use crate::dock::DockPosition;
 use crate::WorkspaceId;
@@ -96,22 +96,16 @@ impl WorkspaceDb {
             WorkspaceLocation,
             bool,
             DockPosition,
-        ) = iife!({
-            if worktree_roots.len() == 0 {
-                self.select_row(sql!(
-                    SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
-                    FROM workspaces
-                    ORDER BY timestamp DESC LIMIT 1))?()?
-            } else {
-                self.select_row_bound(sql!(
-                    SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
-                    FROM workspaces 
-                    WHERE workspace_location = ?))?(&workspace_location)?
-            }
+        ) = 
+            self.select_row_bound(sql!{
+                SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
+                FROM workspaces 
+                WHERE workspace_location = ?
+            })
+            .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
             .context("No workspaces found")
-        })
-        .warn_on_err()
-        .flatten()?;
+            .warn_on_err()
+            .flatten()?;
 
         Some(SerializedWorkspace {
             id: workspace_id,
@@ -205,11 +199,21 @@ impl WorkspaceDb {
         }
     }
 
+    query! {
+        pub fn last_workspace() -> Result<Option<WorkspaceLocation>> {
+            SELECT workspace_location
+            FROM workspaces
+            WHERE workspace_location IS NOT NULL
+            ORDER BY timestamp DESC
+            LIMIT 1
+        }
+    }
+
     fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
-        self.get_pane_group(workspace_id, None)?
+        Ok(self.get_pane_group(workspace_id, None)?
             .into_iter()
             .next()
-            .context("No center pane group")
+            .unwrap_or_else(|| SerializedPaneGroup::Pane(SerializedPane { active: true, children: vec![] })))
     }
 
     fn get_pane_group(
@@ -263,7 +267,7 @@ impl WorkspaceDb {
         // Filter out panes and pane groups which don't have any children or items
         .filter(|pane_group| match pane_group {
             Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
-            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
+            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(), 
             _ => true,
         })
         .collect::<Result<_>>()
@@ -371,6 +375,15 @@ impl WorkspaceDb {
 
         Ok(())
     }
+
+    query!{
+        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
+            UPDATE workspaces
+            SET timestamp = CURRENT_TIMESTAMP
+            WHERE workspace_id = ?
+        }
+    }
+    
 }
 
 #[cfg(test)]

crates/workspace/src/persistence/model.rs 🔗

@@ -106,7 +106,6 @@ impl SerializedPaneGroup {
                         .await
                     {
                         members.push(new_member);
-
                         current_active_pane = current_active_pane.or(active_pane);
                     }
                 }
@@ -115,6 +114,10 @@ impl SerializedPaneGroup {
                     return None;
                 }
 
+                if members.len() == 1 {
+                    return Some((members.remove(0), current_active_pane));
+                }
+
                 Some((
                     Member::Axis(PaneAxis {
                         axis: *axis,
@@ -130,9 +133,10 @@ impl SerializedPaneGroup {
                     .deserialize_to(project, &pane, workspace_id, workspace, cx)
                     .await;
 
-                if pane.read_with(cx, |pane, _| pane.items().next().is_some()) {
+                if pane.read_with(cx, |pane, _| pane.items_len() != 0) {
                     Some((Member::Pane(pane.clone()), active.then(|| pane)))
                 } else {
+                    workspace.update(cx, |workspace, cx| workspace.remove_pane(pane, cx));
                     None
                 }
             }

crates/workspace/src/workspace.rs 🔗

@@ -178,6 +178,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
             }
         }
     });
+
     cx.add_global_action({
         let app_state = Arc::downgrade(&app_state);
         move |_: &NewWindow, cx: &mut MutableAppContext| {
@@ -2167,7 +2168,11 @@ impl Workspace {
     }
 
     pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-        if !active {
+        if active {
+            cx.background()
+                .spawn(persistence::DB.update_timestamp(self.database_id()))
+                .detach();
+        } else {
             for pane in &self.panes {
                 pane.update(cx, |pane, cx| {
                     if let Some(item) = pane.active_item() {
@@ -2281,6 +2286,9 @@ impl Workspace {
         }
 
         if let Some(location) = self.location(cx) {
+            // Load bearing special case:
+            //  - with_local_workspace() relies on this to not have other stuff open
+            //    when you open your log
             if !location.paths().is_empty() {
                 let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
                 let center_group = build_serialized_pane_group(&self.center.root, cx);
@@ -2308,9 +2316,14 @@ impl Workspace {
     ) {
         cx.spawn(|mut cx| async move {
             if let Some(workspace) = workspace.upgrade(&cx) {
-                let (project, dock_pane_handle) = workspace.read_with(&cx, |workspace, _| {
-                    (workspace.project().clone(), workspace.dock_pane().clone())
-                });
+                let (project, dock_pane_handle, old_center_pane) =
+                    workspace.read_with(&cx, |workspace, _| {
+                        (
+                            workspace.project().clone(),
+                            workspace.dock_pane().clone(),
+                            workspace.last_active_center_pane.clone(),
+                        )
+                    });
 
                 serialized_workspace
                     .dock_pane
@@ -2346,11 +2359,14 @@ impl Workspace {
                             cx.focus(workspace.panes.last().unwrap().clone());
                         }
                     } else {
-                        cx.focus_self();
+                        let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
+                        if let Some(old_center_handle) = old_center_handle {
+                            cx.focus(old_center_handle)
+                        } else {
+                            cx.focus_self()
+                        }
                     }
 
-                    // Note, if this is moved after 'set_dock_position'
-                    // it causes an infinite loop.
                     if workspace.left_sidebar().read(cx).is_open()
                         != serialized_workspace.left_sidebar_open
                     {
@@ -2604,6 +2620,10 @@ pub fn activate_workspace_for_project(
     None
 }
 
+pub fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
+    DB.last_workspace().log_err().flatten()
+}
+
 #[allow(clippy::type_complexity)]
 pub fn open_paths(
     abs_paths: &[PathBuf],

crates/zed/src/main.rs 🔗

@@ -169,7 +169,7 @@ fn main() {
             cx.platform().activate(true);
             let paths = collect_path_args();
             if paths.is_empty() {
-                cx.dispatch_global_action(NewFile);
+                restore_or_create_workspace(cx);
             } else {
                 cx.dispatch_global_action(OpenPaths { paths });
             }
@@ -178,7 +178,7 @@ fn main() {
                 cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
                     .detach();
             } else {
-                cx.dispatch_global_action(NewFile);
+                restore_or_create_workspace(cx);
             }
             cx.spawn(|cx| async move {
                 while let Some(connection) = cli_connections_rx.next().await {
@@ -202,6 +202,16 @@ fn main() {
     });
 }
 
+fn restore_or_create_workspace(cx: &mut gpui::MutableAppContext) {
+    if let Some(location) = workspace::last_opened_workspace_paths() {
+        cx.dispatch_global_action(OpenPaths {
+            paths: location.paths().as_ref().clone(),
+        })
+    } else {
+        cx.dispatch_global_action(NewFile);
+    }
+}
+
 fn init_paths() {
     std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path");
     std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path");