From cf74d653bdfda6d30e850849b5d5bb92e82b39df Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 9 Feb 2025 23:29:29 +0200 Subject: [PATCH] Fix outline panel issues in a multi-worktree set-up (#24538) Closes https://github.com/zed-industries/zed/issues/22993 Properly calculates depth and maintains worktree order, when displaying multiple worktrees in the outline panel. Release Notes: - Fixed outline panel issues in a multi-worktree set-up --- crates/outline_panel/src/outline_panel.rs | 342 +++++++++++++++++++--- 1 file changed, 298 insertions(+), 44 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 54e4a2bde0..b5b637ce04 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2,6 +2,7 @@ mod outline_panel_settings; use std::{ cmp, + collections::BTreeMap, hash::Hash, ops::Range, path::{Path, PathBuf, MAIN_SEPARATOR_STR}, @@ -2626,7 +2627,7 @@ impl OutlinePanel { .spawn(async move { let mut processed_external_buffers = HashSet::default(); let mut new_worktree_entries = - HashMap::>::default(); + BTreeMap::>::default(); let mut worktree_excerpts = HashMap::< WorktreeId, HashMap)>, @@ -3492,7 +3493,8 @@ impl OutlinePanel { .copied() .unwrap_or(0); while let Some(parent) = parent_dirs.last() { - if directory_entry.entry.path.starts_with(&parent.path) { + if !is_root && directory_entry.entry.path.starts_with(&parent.path) + { break; } parent_dirs.pop(); @@ -5156,6 +5158,7 @@ mod tests { use project::FakeFs; use search::project_search::{self, perform_project_search}; use serde_json::json; + use workspace::OpenVisible; use super::*; @@ -5212,7 +5215,7 @@ mod tests { }); }); - let all_matches = r#"/ + let all_matches = r#"/rust-analyzer/ crates/ ide/src/ inlay_hints/ @@ -5247,9 +5250,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches( "search: match config.param_names_for_lifetime_elision_hints {" @@ -5261,9 +5266,11 @@ mod tests { outline_panel.select_parent(&SelectParent, window, cx); assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches("fn_lifetime_fn.rs") ); @@ -5277,12 +5284,14 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), format!( - r#"/ + r#"/rust-analyzer/ crates/ ide/src/ inlay_hints/ @@ -5312,9 +5321,11 @@ mod tests { outline_panel.select_parent(&SelectParent, window, cx); assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches("inlay_hints/") ); @@ -5324,9 +5335,11 @@ mod tests { outline_panel.select_parent(&SelectParent, window, cx); assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches("ide/src/") ); @@ -5341,12 +5354,14 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), format!( - r#"/ + r#"/rust-analyzer/ crates/ ide/src/{SELECTED_MARKER} rust-analyzer/src/ @@ -5367,9 +5382,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches("ide/src/") ); @@ -5426,7 +5443,7 @@ mod tests { ); }); }); - let all_matches = r#"/ + let all_matches = r#"/rust-analyzer/ crates/ ide/src/ inlay_hints/ @@ -5453,9 +5470,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, + cx, ), all_matches, ); @@ -5474,12 +5493,15 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, + cx, ), all_matches .lines() + .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out .filter(|item| item.contains(filter_text)) .collect::>() .join("\n"), @@ -5497,9 +5519,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, + cx, ), all_matches, ); @@ -5556,7 +5580,7 @@ mod tests { ); }); }); - let all_matches = r#"/ + let all_matches = r#"/rust-analyzer/ crates/ ide/src/ inlay_hints/ @@ -5598,9 +5622,11 @@ mod tests { outline_panel.update_in(cx, |outline_panel, window, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), + cx, ), select_first_in_all_matches(initial_outline_selection) ); @@ -5619,9 +5645,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), + cx, ), select_first_in_all_matches(navigated_outline_selection) ); @@ -5655,9 +5683,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), + cx, ), select_first_in_all_matches(next_navigated_outline_selection) ); @@ -5690,9 +5720,11 @@ mod tests { ); assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), + cx, ), "fn_lifetime_fn.rs <==== selected" ); @@ -5704,6 +5736,176 @@ mod tests { }); } + #[gpui::test] + async fn test_multiple_workrees(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "one": { + "a.txt": "aaa aaa" + }, + "two": { + "b.txt": "a aaa" + } + + }), + ) + .await; + let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await; + let workspace = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let outline_panel = outline_panel(&workspace, cx); + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.set_active(true, window, cx) + }); + + let items = workspace + .update(cx, |workspace, window, cx| { + workspace.open_paths( + vec![PathBuf::from("/root/two")], + OpenVisible::OnlyDirectories, + None, + window, + cx, + ) + }) + .unwrap() + .await; + assert_eq!(items.len(), 1, "Were opening another worktree directory"); + assert!( + items[0].is_none(), + "Directory should be opened successfully" + ); + + workspace + .update(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::default(), + window, + cx, + ) + }) + .unwrap(); + let search_view = workspace + .update(cx, |workspace, _, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view expected to appear after new search event trigger") + }) + .unwrap(); + + let query = "aaa"; + perform_project_search(&search_view, query, cx); + search_view.update(cx, |search_view, cx| { + search_view + .results_editor() + .update(cx, |results_editor, cx| { + assert_eq!( + results_editor.display_text(cx).match_indices(query).count(), + 3 + ); + }); + }); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + r#"/root/one/ + a.txt + search: aaa aaa <==== selected + search: aaa aaa +/root/two/ + b.txt + search: a aaa"# + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.select_prev(&SelectPrev, window, cx); + outline_panel.open(&Open, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + r#"/root/one/ + a.txt <==== selected +/root/two/ + b.txt + search: a aaa"# + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.select_next(&SelectNext, window, cx); + outline_panel.open(&Open, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + r#"/root/one/ + a.txt +/root/two/ <==== selected"# + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.open(&Open, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + r#"/root/one/ + a.txt +/root/two/ <==== selected + b.txt + search: a aaa"# + ); + }); + } + #[gpui::test] async fn test_navigating_in_singleton(cx: &mut TestAppContext) { init_test(cx); @@ -5769,9 +5971,11 @@ struct OutlineEntryExcerpt { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5794,9 +5998,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5819,9 +6025,11 @@ outline: struct OutlineEntryExcerpt <==== selected outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5844,9 +6052,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5869,9 +6079,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5894,9 +6106,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5919,9 +6133,11 @@ outline: struct OutlineEntryExcerpt <==== selected outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5944,9 +6160,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5969,9 +6187,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5994,9 +6214,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -6019,9 +6241,11 @@ outline: struct OutlineEntryExcerpt <==== selected outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -6123,11 +6347,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } <==== selected @@ -6158,11 +6384,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } @@ -6184,11 +6412,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } @@ -6214,11 +6444,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } @@ -6243,11 +6475,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } @@ -6294,9 +6528,11 @@ outline: struct OutlineEntryExcerpt } fn display_entries( + project: &Entity, multi_buffer_snapshot: &MultiBufferSnapshot, cached_entries: &[CachedEntry], selected_entry: Option<&PanelEntry>, + cx: &mut App, ) -> String { let mut display_string = String::new(); for entry in cached_entries { @@ -6311,15 +6547,33 @@ outline: struct OutlineEntryExcerpt FsEntry::ExternalFile(_) => { panic!("Did not cover external files with tests") } - FsEntry::Directory(directory) => format!( - "{}/", - directory - .entry - .path - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_default() - ), + FsEntry::Directory(directory) => { + match project + .read(cx) + .worktree_for_id(directory.worktree_id, cx) + .and_then(|worktree| { + if worktree.read(cx).root_entry() == Some(&directory.entry.entry) { + Some(worktree.read(cx).abs_path()) + } else { + None + } + }) { + Some(root_path) => format!( + "{}/{}", + root_path.display(), + directory.entry.path.display(), + ), + None => format!( + "{}/", + directory + .entry + .path + .file_name() + .unwrap_or_default() + .to_string_lossy() + ), + } + } FsEntry::File(file) => file .entry .path