From f05095a6dd64d16787bf4152f02436e82577d8f9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 22 Jul 2023 02:24:46 +0300 Subject: [PATCH] Focus project panel on directory select --- 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(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b3255df812..6b905a1faa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -259,6 +259,7 @@ pub enum Event { LanguageServerLog(LanguageServerId, String), Notification(String), ActiveEntryChanged(Option), + ActivateProjectPanel, WorktreeAdded, WorktreeRemoved(WorktreeId), WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet), diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 87b0d21a9f..2a6da7db28 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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)) } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 2785c1a871..e3109102d1 100644 --- a/crates/terminal/src/terminal.rs +++ b/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(); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index bee1107d6d..e108a05ccc 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/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::() { - 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::()) + { + 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(()) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5c1a75e97a..1e9e431f9d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -898,6 +898,18 @@ impl Workspace { pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) where T::Event: std::fmt::Debug, + { + self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {}) + } + + pub fn add_panel_with_extra_event_handler( + &mut self, + panel: ViewHandle, + cx: &mut ViewContext, + handler: F, + ) where + T::Event: std::fmt::Debug, + F: Fn(&mut Self, &ViewHandle, &T::Event, &mut ViewContext) + '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::>(); + } + }); + 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 { let project = self.project().read(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8a2691da15..db7c57a89c 100644 --- a/crates/zed/src/zed.rs +++ b/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::(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::() + .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::(), + workspace.right_dock().read(cx).panel::(), + workspace.bottom_dock().read(cx).panel::(), + ] + .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::>(); @@ -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::>(); @@ -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::>(); @@ -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::>(); @@ -1237,7 +1268,6 @@ mod tests { assert_eq!( workspace - .read(cx) .active_pane() .read(cx) .active_item()