Focus project panel on directory select

Kirill Bulatov created

Change summary

crates/project/src/project.rs             |   1 
crates/project_panel/src/project_panel.rs |   9 +
crates/terminal/src/terminal.rs           |   4 
crates/terminal_view/src/terminal_view.rs |  71 +++++++++++------
crates/workspace/src/workspace.rs         | 102 ++++++++++++++++--------
crates/zed/src/zed.rs                     |  92 ++++++++++++++-------
6 files changed, 184 insertions(+), 95 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -259,6 +259,7 @@ pub enum Event {
     LanguageServerLog(LanguageServerId, String),
     Notification(String),
     ActiveEntryChanged(Option<ProjectEntryId>),
+    ActivateProjectPanel,
     WorktreeAdded,
     WorktreeRemoved(WorktreeId),
     WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),

crates/project_panel/src/project_panel.rs 🔗

@@ -174,6 +174,7 @@ pub enum Event {
     NewSearchInDirectory {
         dir_entry: Entry,
     },
+    ActivatePanel,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -200,6 +201,9 @@ impl ProjectPanel {
                         cx.notify();
                     }
                 }
+                project::Event::ActivateProjectPanel => {
+                    cx.emit(Event::ActivatePanel);
+                }
                 project::Event::WorktreeRemoved(id) => {
                     this.expanded_dir_ids.remove(id);
                     this.update_visible_entries(None, cx);
@@ -1014,7 +1018,10 @@ impl ProjectPanel {
         None
     }
 
-    fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> {
+    pub fn selected_entry<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> Option<(&'a Worktree, &'a project::Entry)> {
         let (worktree, entry) = self.selected_entry_handle(cx)?;
         Some((worktree.read(cx), entry))
     }

crates/terminal/src/terminal.rs 🔗

@@ -73,7 +73,9 @@ const DEBUG_CELL_WIDTH: f32 = 5.;
 const DEBUG_LINE_HEIGHT: f32 = 5.;
 
 lazy_static! {
-    // Regex Copied from alacritty's ui_config.rs
+    // Regex Copied from alacritty's ui_config.rs and modified its declaration slightly:
+    // * avoid Rust-specific escaping.
+    // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
     static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
 
     static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-~]+").unwrap();

crates/terminal_view/src/terminal_view.rs 🔗

@@ -187,37 +187,56 @@ impl TerminalView {
                     }
                     let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
                     if let Some(path) = potential_abs_paths.into_iter().next() {
-                        let visible = path.path_like.is_dir();
+                        let is_dir = path.path_like.is_dir();
                         let task_workspace = workspace.clone();
                         cx.spawn(|_, mut cx| async move {
-                            let opened_item = task_workspace
+                            let opened_items = task_workspace
                                 .update(&mut cx, |workspace, cx| {
-                                    workspace.open_abs_path(path.path_like, visible, cx)
+                                    workspace.open_paths(vec![path.path_like], is_dir, cx)
                                 })
                                 .context("workspace update")?
-                                .await
-                                .context("workspace update")?;
-                            if let Some(row) = path.row {
-                                let col = path.column.unwrap_or(0);
-                                if let Some(active_editor) = opened_item.downcast::<Editor>() {
-                                    active_editor
-                                        .downgrade()
-                                        .update(&mut cx, |editor, cx| {
-                                            let snapshot = editor.snapshot(cx).display_snapshot;
-                                            let point = snapshot.buffer_snapshot.clip_point(
-                                                language::Point::new(
-                                                    row.saturating_sub(1),
-                                                    col.saturating_sub(1),
-                                                ),
-                                                Bias::Left,
-                                            );
-                                            editor.change_selections(
-                                                Some(Autoscroll::center()),
-                                                cx,
-                                                |s| s.select_ranges([point..point]),
-                                            );
-                                        })
-                                        .log_err();
+                                .await;
+                            anyhow::ensure!(
+                                opened_items.len() == 1,
+                                "For a single path open, expected single opened item"
+                            );
+                            let opened_item = opened_items
+                                .into_iter()
+                                .next()
+                                .unwrap()
+                                .transpose()
+                                .context("path open")?;
+                            if is_dir {
+                                task_workspace.update(&mut cx, |workspace, cx| {
+                                    workspace.project().update(cx, |_, cx| {
+                                        cx.emit(project::Event::ActivateProjectPanel);
+                                    })
+                                })?;
+                            } else {
+                                if let Some(row) = path.row {
+                                    let col = path.column.unwrap_or(0);
+                                    if let Some(active_editor) =
+                                        opened_item.and_then(|item| item.downcast::<Editor>())
+                                    {
+                                        active_editor
+                                            .downgrade()
+                                            .update(&mut cx, |editor, cx| {
+                                                let snapshot = editor.snapshot(cx).display_snapshot;
+                                                let point = snapshot.buffer_snapshot.clip_point(
+                                                    language::Point::new(
+                                                        row.saturating_sub(1),
+                                                        col.saturating_sub(1),
+                                                    ),
+                                                    Bias::Left,
+                                                );
+                                                editor.change_selections(
+                                                    Some(Autoscroll::center()),
+                                                    cx,
+                                                    |s| s.select_ranges([point..point]),
+                                                );
+                                            })
+                                            .log_err();
+                                    }
                                 }
                             }
                             anyhow::Ok(())

crates/workspace/src/workspace.rs 🔗

@@ -898,6 +898,18 @@ impl Workspace {
     pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
     where
         T::Event: std::fmt::Debug,
+    {
+        self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
+    }
+
+    pub fn add_panel_with_extra_event_handler<T: Panel, F>(
+        &mut self,
+        panel: ViewHandle<T>,
+        cx: &mut ViewContext<Self>,
+        handler: F,
+    ) where
+        T::Event: std::fmt::Debug,
+        F: Fn(&mut Self, &ViewHandle<T>, &T::Event, &mut ViewContext<Self>) + 'static,
     {
         let dock = match panel.position(cx) {
             DockPosition::Left => &self.left_dock,
@@ -965,6 +977,8 @@ impl Workspace {
                     }
                     this.update_active_view_for_followers(cx);
                     cx.notify();
+                } else {
+                    handler(this, &panel, event, cx)
                 }
             }
         }));
@@ -1417,45 +1431,65 @@ impl Workspace {
         // Sort the paths to ensure we add worktrees for parents before their children.
         abs_paths.sort_unstable();
         cx.spawn(|this, mut cx| async move {
-            let mut project_paths = Vec::new();
-            for path in &abs_paths {
-                if let Some(project_path) = this
+            let mut tasks = Vec::with_capacity(abs_paths.len());
+            for abs_path in &abs_paths {
+                let project_path = match this
                     .update(&mut cx, |this, cx| {
-                        Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
+                        Workspace::project_path_for_path(
+                            this.project.clone(),
+                            abs_path,
+                            visible,
+                            cx,
+                        )
                     })
                     .log_err()
                 {
-                    project_paths.push(project_path.await.log_err());
-                } else {
-                    project_paths.push(None);
-                }
-            }
+                    Some(project_path) => project_path.await.log_err(),
+                    None => None,
+                };
 
-            let tasks = abs_paths
-                .iter()
-                .cloned()
-                .zip(project_paths.into_iter())
-                .map(|(abs_path, project_path)| {
-                    let this = this.clone();
-                    cx.spawn(|mut cx| {
-                        let fs = fs.clone();
-                        async move {
-                            let (_worktree, project_path) = project_path?;
-                            if fs.is_file(&abs_path).await {
-                                Some(
-                                    this.update(&mut cx, |this, cx| {
-                                        this.open_path(project_path, None, true, cx)
+                let this = this.clone();
+                let task = cx.spawn(|mut cx| {
+                    let fs = fs.clone();
+                    let abs_path = abs_path.clone();
+                    async move {
+                        let (worktree, project_path) = project_path?;
+                        if fs.is_file(&abs_path).await {
+                            Some(
+                                this.update(&mut cx, |this, cx| {
+                                    this.open_path(project_path, None, true, cx)
+                                })
+                                .log_err()?
+                                .await,
+                            )
+                        } else {
+                            this.update(&mut cx, |workspace, cx| {
+                                let worktree = worktree.read(cx);
+                                let worktree_abs_path = worktree.abs_path();
+                                let entry_id = if abs_path == worktree_abs_path.as_ref() {
+                                    worktree.root_entry()
+                                } else {
+                                    abs_path
+                                        .strip_prefix(worktree_abs_path.as_ref())
+                                        .ok()
+                                        .and_then(|relative_path| {
+                                            worktree.entry_for_path(relative_path)
+                                        })
+                                }
+                                .map(|entry| entry.id);
+                                if let Some(entry_id) = entry_id {
+                                    workspace.project().update(cx, |_, cx| {
+                                        cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
                                     })
-                                    .log_err()?
-                                    .await,
-                                )
-                            } else {
-                                None
-                            }
+                                }
+                            })
+                            .log_err()?;
+                            None
                         }
-                    })
-                })
-                .collect::<Vec<_>>();
+                    }
+                });
+                tasks.push(task);
+            }
 
             futures::future::join_all(tasks).await
         })
@@ -3009,10 +3043,6 @@ impl Workspace {
         self.database_id
     }
 
-    pub fn push_subscription(&mut self, subscription: Subscription) {
-        self.subscriptions.push(subscription)
-    }
-
     fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
         let project = self.project().read(cx);
 

crates/zed/src/zed.rs 🔗

@@ -339,29 +339,21 @@ pub fn initialize_workspace(
         let (project_panel, terminal_panel, assistant_panel) =
             futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
 
-        cx.update(|cx| {
-            if let Some(workspace) = workspace_handle.upgrade(cx) {
-                cx.update_window(project_panel.window_id(), |cx| {
-                    workspace.update(cx, |workspace, cx| {
-                        let project_panel_subscription =
-                            cx.subscribe(&project_panel, move |workspace, _, event, cx| {
-                                if let project_panel::Event::NewSearchInDirectory { dir_entry } =
-                                    event
-                                {
-                                    search::ProjectSearchView::new_search_in_directory(
-                                        workspace, dir_entry, cx,
-                                    )
-                                }
-                            });
-                        workspace.push_subscription(project_panel_subscription);
-                    });
-                });
-            }
-        });
-
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
-            workspace.add_panel(project_panel, cx);
+            workspace.add_panel_with_extra_event_handler(
+                project_panel,
+                cx,
+                |workspace, _, event, cx| match event {
+                    project_panel::Event::NewSearchInDirectory { dir_entry } => {
+                        search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx)
+                    }
+                    project_panel::Event::ActivatePanel => {
+                        workspace.focus_panel::<ProjectPanel>(cx);
+                    }
+                    _ => {}
+                },
+            );
             workspace.add_panel(terminal_panel, cx);
             workspace.add_panel(assistant_panel, cx);
 
@@ -1106,8 +1098,46 @@ mod tests {
             )
             .await;
 
-        let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
+            .await
+            .unwrap();
+        assert_eq!(cx.window_ids().len(), 1);
+        let workspace = cx
+            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
+            .unwrap()
+            .downcast::<Workspace>()
+            .unwrap();
+
+        #[track_caller]
+        fn assert_project_panel_selection(
+            workspace: &Workspace,
+            expected_worktree_path: &Path,
+            expected_entry_path: &Path,
+            cx: &AppContext,
+        ) {
+            let project_panel = [
+                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
+                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
+                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
+            ]
+            .into_iter()
+            .find_map(std::convert::identity)
+            .expect("found no project panels")
+            .read(cx);
+            let (selected_worktree, selected_entry) = project_panel
+                .selected_entry(cx)
+                .expect("project panel should have a selected entry");
+            assert_eq!(
+                selected_worktree.abs_path().as_ref(),
+                expected_worktree_path,
+                "Unexpected project panel selected worktree path"
+            );
+            assert_eq!(
+                selected_entry.path.as_ref(),
+                expected_entry_path,
+                "Unexpected project panel selected entry path"
+            );
+        }
 
         // Open a file within an existing worktree.
         workspace
@@ -1116,9 +1146,10 @@ mod tests {
             })
             .await;
         cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
             assert_eq!(
                 workspace
-                    .read(cx)
                     .active_pane()
                     .read(cx)
                     .active_item()
@@ -1139,8 +1170,9 @@ mod tests {
             })
             .await;
         cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
             let worktree_roots = workspace
-                .read(cx)
                 .worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
@@ -1153,7 +1185,6 @@ mod tests {
             );
             assert_eq!(
                 workspace
-                    .read(cx)
                     .active_pane()
                     .read(cx)
                     .active_item()
@@ -1174,8 +1205,9 @@ mod tests {
             })
             .await;
         cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
             let worktree_roots = workspace
-                .read(cx)
                 .worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
@@ -1188,7 +1220,6 @@ mod tests {
             );
             assert_eq!(
                 workspace
-                    .read(cx)
                     .active_pane()
                     .read(cx)
                     .active_item()
@@ -1209,8 +1240,9 @@ mod tests {
             })
             .await;
         cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
             let worktree_roots = workspace
-                .read(cx)
                 .worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
@@ -1223,7 +1255,6 @@ mod tests {
             );
 
             let visible_worktree_roots = workspace
-                .read(cx)
                 .visible_worktrees(cx)
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
@@ -1237,7 +1268,6 @@ mod tests {
 
             assert_eq!(
                 workspace
-                    .read(cx)
                     .active_pane()
                     .read(cx)
                     .active_item()