Improve project panel autoreveal ergonomics (#3604)
Part of https://linear.app/zed-industries/issue/Z-1386/add-equivalent-setting-to-vscodes-auto-reveal-exclude Deals with https://github.com/zed-industries/community/issues/800 * add a `project_panel::auto_reveal_entries` config entry to allow disabling auto reveal in project panel (auto reveal is enabled by default) * add a `pane::RevealInProjectPanel` action (and a pane tab context menu entry) to manually reveal any file entry * stop auto revealing gitignored directories at all We can add the auto reveal exclude globs later, if needed, but let's try to keep the config simpler and start with a more minimalist approach. Release Notes: - Improved project panel auto reveal mechanics: gitignored files are not auto revealed anymore; a `auto_reveal_entries = true` config option for `project_panel` is added; a `pane::RevealInProjectPanel` action and a corresponding buffer tab context menu were added
This commit is contained in:
@@ -143,7 +143,11 @@
|
||||
// Whether to show the git status in the project panel.
|
||||
"git_status": true,
|
||||
// Amount of indentation for nested items.
|
||||
"indent_size": 20
|
||||
"indent_size": 20,
|
||||
// Whether to reveal it in the project panel automatically,
|
||||
// when a corresponding project entry becomes active.
|
||||
// Gitignored entries are never auto revealed.
|
||||
"auto_reveal_entries": true
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
|
||||
@@ -1189,7 +1189,7 @@ impl CursorPosition {
|
||||
impl Render for CursorPosition {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().when_some(self.position, |el, position| {
|
||||
let mut text = format!(
|
||||
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
||||
|
||||
@@ -301,6 +301,7 @@ pub enum Event {
|
||||
CollaboratorJoined(proto::PeerId),
|
||||
CollaboratorLeft(proto::PeerId),
|
||||
RefreshInlayHints,
|
||||
RevealInProjectPanel(ProjectEntryId),
|
||||
}
|
||||
|
||||
pub enum LanguageServerState {
|
||||
|
||||
@@ -299,6 +299,7 @@ pub enum Event {
|
||||
CollaboratorJoined(proto::PeerId),
|
||||
CollaboratorLeft(proto::PeerId),
|
||||
RefreshInlayHints,
|
||||
RevealInProjectPanel(ProjectEntryId),
|
||||
}
|
||||
|
||||
pub enum LanguageServerState {
|
||||
|
||||
@@ -199,14 +199,14 @@ impl ProjectPanel {
|
||||
.detach();
|
||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||
project::Event::ActiveEntryChanged(Some(entry_id)) => {
|
||||
if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
|
||||
{
|
||||
this.expand_entry(worktree_id, *entry_id, cx);
|
||||
this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
|
||||
this.autoscroll(cx);
|
||||
cx.notify();
|
||||
if settings::get::<ProjectPanelSettings>(cx).auto_reveal_entries {
|
||||
this.reveal_entry(project, *entry_id, true, cx);
|
||||
}
|
||||
}
|
||||
project::Event::RevealInProjectPanel(entry_id) => {
|
||||
this.reveal_entry(project, *entry_id, false, cx);
|
||||
cx.emit(Event::ActivatePanel);
|
||||
}
|
||||
project::Event::ActivateProjectPanel => {
|
||||
cx.emit(Event::ActivatePanel);
|
||||
}
|
||||
@@ -1531,6 +1531,31 @@ impl ProjectPanel {
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any_named("project panel entry")
|
||||
}
|
||||
|
||||
fn reveal_entry(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
entry_id: ProjectEntryId,
|
||||
skip_ignored: bool,
|
||||
cx: &mut ViewContext<'_, '_, ProjectPanel>,
|
||||
) {
|
||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
if skip_ignored
|
||||
&& worktree
|
||||
.entry_for_id(entry_id)
|
||||
.map_or(true, |entry| entry.is_ignored)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = worktree.id();
|
||||
self.expand_entry(worktree_id, entry_id, cx);
|
||||
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ProjectPanel {
|
||||
@@ -2956,6 +2981,445 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
|
||||
project_panel_settings.auto_reveal_entries = Some(false)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/project_root",
|
||||
json!({
|
||||
".git": {},
|
||||
".gitignore": "**/gitignored_dir",
|
||||
"dir_1": {
|
||||
"file_1.py": "# File 1_1 contents",
|
||||
"file_2.py": "# File 1_2 contents",
|
||||
"file_3.py": "# File 1_3 contents",
|
||||
"gitignored_dir": {
|
||||
"file_a.py": "# File contents",
|
||||
"file_b.py": "# File contents",
|
||||
"file_c.py": "# File contents",
|
||||
},
|
||||
},
|
||||
"dir_2": {
|
||||
"file_1.py": "# File 2_1 contents",
|
||||
"file_2.py": "# File 2_2 contents",
|
||||
"file_3.py": "# File 2_3 contents",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
|
||||
let workspace = cx
|
||||
.add_window(|cx| Workspace::test_new(project.clone(), cx))
|
||||
.root(cx);
|
||||
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
]
|
||||
);
|
||||
|
||||
let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
|
||||
.expect("dir 1 file is not ignored and should have an entry");
|
||||
let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
|
||||
.expect("dir 2 file is not ignored and should have an entry");
|
||||
let gitignored_dir_file =
|
||||
find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
|
||||
assert_eq!(
|
||||
gitignored_dir_file, None,
|
||||
"File in the gitignored dir should not have an entry before its dir is toggled"
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "project_root/dir_1", cx);
|
||||
toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir <== selected",
|
||||
" file_a.py",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"Should show gitignored dir file list in the project panel"
|
||||
);
|
||||
let gitignored_dir_file =
|
||||
find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
|
||||
.expect("after gitignored dir got opened, a file entry should be present");
|
||||
|
||||
toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
|
||||
toggle_expand_dir(&panel, "project_root/dir_1", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1 <== selected",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"Should hide all dir contents again and prepare for the auto reveal test"
|
||||
);
|
||||
|
||||
for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1 <== selected",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
|
||||
);
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
|
||||
project_panel_settings.auto_reveal_entries = Some(true)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"When auto reveal is enabled, not ignored dir_1 entry should be revealed"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"When auto reveal is enabled, not ignored dir_2 entry should be revealed"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(
|
||||
gitignored_dir_file,
|
||||
)))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir",
|
||||
" file_a.py <== selected",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"When a gitignored entry is explicitly revealed, it should be shown in the project tree"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
|
||||
project_panel_settings.auto_reveal_entries = Some(false)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/project_root",
|
||||
json!({
|
||||
".git": {},
|
||||
".gitignore": "**/gitignored_dir",
|
||||
"dir_1": {
|
||||
"file_1.py": "# File 1_1 contents",
|
||||
"file_2.py": "# File 1_2 contents",
|
||||
"file_3.py": "# File 1_3 contents",
|
||||
"gitignored_dir": {
|
||||
"file_a.py": "# File contents",
|
||||
"file_b.py": "# File contents",
|
||||
"file_c.py": "# File contents",
|
||||
},
|
||||
},
|
||||
"dir_2": {
|
||||
"file_1.py": "# File 2_1 contents",
|
||||
"file_2.py": "# File 2_2 contents",
|
||||
"file_3.py": "# File 2_3 contents",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
|
||||
let workspace = cx
|
||||
.add_window(|cx| Workspace::test_new(project.clone(), cx))
|
||||
.root(cx);
|
||||
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
]
|
||||
);
|
||||
|
||||
let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
|
||||
.expect("dir 1 file is not ignored and should have an entry");
|
||||
let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
|
||||
.expect("dir 2 file is not ignored and should have an entry");
|
||||
let gitignored_dir_file =
|
||||
find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
|
||||
assert_eq!(
|
||||
gitignored_dir_file, None,
|
||||
"File in the gitignored dir should not have an entry before its dir is toggled"
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "project_root/dir_1", cx);
|
||||
toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir <== selected",
|
||||
" file_a.py",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"Should show gitignored dir file list in the project panel"
|
||||
);
|
||||
let gitignored_dir_file =
|
||||
find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
|
||||
.expect("after gitignored dir got opened, a file entry should be present");
|
||||
|
||||
toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
|
||||
toggle_expand_dir(&panel, "project_root/dir_1", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1 <== selected",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"Should hide all dir contents again and prepare for the explicit reveal test"
|
||||
);
|
||||
|
||||
for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1 <== selected",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
|
||||
);
|
||||
}
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir",
|
||||
" file_a.py <== selected",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
|
||||
);
|
||||
}
|
||||
|
||||
fn toggle_expand_dir(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
@@ -2993,10 +3457,27 @@ mod tests {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("no worktree for path {:?}", path);
|
||||
panic!("no worktree for path {path:?}");
|
||||
});
|
||||
}
|
||||
|
||||
fn find_project_entry(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Option<ProjectEntryId> {
|
||||
let path = path.as_ref();
|
||||
panel.update(cx, |panel, cx| {
|
||||
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
return worktree.entry_for_path(relative_path).map(|entry| entry.id);
|
||||
}
|
||||
}
|
||||
panic!("no worktree for path {path:?}");
|
||||
})
|
||||
}
|
||||
|
||||
fn visible_entries_as_strings(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
range: Range<usize>,
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct ProjectPanelSettings {
|
||||
pub folder_icons: bool,
|
||||
pub git_status: bool,
|
||||
pub indent_size: f32,
|
||||
pub auto_reveal_entries: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -28,6 +29,7 @@ pub struct ProjectPanelSettingsContent {
|
||||
pub folder_icons: Option<bool>,
|
||||
pub git_status: Option<bool>,
|
||||
pub indent_size: Option<f32>,
|
||||
pub auto_reveal_entries: Option<bool>,
|
||||
}
|
||||
|
||||
impl Setting for ProjectPanelSettings {
|
||||
|
||||
@@ -184,14 +184,14 @@ impl ProjectPanel {
|
||||
|
||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||
project::Event::ActiveEntryChanged(Some(entry_id)) => {
|
||||
if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
|
||||
{
|
||||
this.expand_entry(worktree_id, *entry_id, cx);
|
||||
this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
|
||||
this.autoscroll(cx);
|
||||
cx.notify();
|
||||
if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
|
||||
this.reveal_entry(project, *entry_id, true, cx);
|
||||
}
|
||||
}
|
||||
project::Event::RevealInProjectPanel(entry_id) => {
|
||||
this.reveal_entry(project, *entry_id, false, cx);
|
||||
cx.emit(Event::ActivatePanel);
|
||||
}
|
||||
project::Event::ActivateProjectPanel => {
|
||||
cx.emit(Event::ActivatePanel);
|
||||
}
|
||||
@@ -1456,6 +1456,31 @@ impl ProjectPanel {
|
||||
dispatch_context.add(identifier);
|
||||
dispatch_context
|
||||
}
|
||||
|
||||
fn reveal_entry(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
entry_id: ProjectEntryId,
|
||||
skip_ignored: bool,
|
||||
cx: &mut ViewContext<'_, ProjectPanel>,
|
||||
) {
|
||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
if skip_ignored
|
||||
&& worktree
|
||||
.entry_for_id(entry_id)
|
||||
.map_or(true, |entry| entry.is_ignored)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = worktree.id();
|
||||
self.expand_entry(worktree_id, entry_id, cx);
|
||||
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectPanel {
|
||||
@@ -2876,6 +2901,447 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
|
||||
project_panel_settings.auto_reveal_entries = Some(false)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/project_root",
|
||||
json!({
|
||||
".git": {},
|
||||
".gitignore": "**/gitignored_dir",
|
||||
"dir_1": {
|
||||
"file_1.py": "# File 1_1 contents",
|
||||
"file_2.py": "# File 1_2 contents",
|
||||
"file_3.py": "# File 1_3 contents",
|
||||
"gitignored_dir": {
|
||||
"file_a.py": "# File contents",
|
||||
"file_b.py": "# File contents",
|
||||
"file_c.py": "# File contents",
|
||||
},
|
||||
},
|
||||
"dir_2": {
|
||||
"file_1.py": "# File 2_1 contents",
|
||||
"file_2.py": "# File 2_2 contents",
|
||||
"file_3.py": "# File 2_3 contents",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
]
|
||||
);
|
||||
|
||||
let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
|
||||
.expect("dir 1 file is not ignored and should have an entry");
|
||||
let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
|
||||
.expect("dir 2 file is not ignored and should have an entry");
|
||||
let gitignored_dir_file =
|
||||
find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
|
||||
assert_eq!(
|
||||
gitignored_dir_file, None,
|
||||
"File in the gitignored dir should not have an entry before its dir is toggled"
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "project_root/dir_1", cx);
|
||||
toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir <== selected",
|
||||
" file_a.py",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"Should show gitignored dir file list in the project panel"
|
||||
);
|
||||
let gitignored_dir_file =
|
||||
find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
|
||||
.expect("after gitignored dir got opened, a file entry should be present");
|
||||
|
||||
toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
|
||||
toggle_expand_dir(&panel, "project_root/dir_1", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1 <== selected",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"Should hide all dir contents again and prepare for the auto reveal test"
|
||||
);
|
||||
|
||||
for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1 <== selected",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
|
||||
);
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
|
||||
project_panel_settings.auto_reveal_entries = Some(true)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"When auto reveal is enabled, not ignored dir_1 entry should be revealed"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"When auto reveal is enabled, not ignored dir_2 entry should be revealed"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(
|
||||
gitignored_dir_file,
|
||||
)))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir",
|
||||
" file_a.py <== selected",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"When a gitignored entry is explicitly revealed, it should be shown in the project tree"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
|
||||
project_panel_settings.auto_reveal_entries = Some(false)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/project_root",
|
||||
json!({
|
||||
".git": {},
|
||||
".gitignore": "**/gitignored_dir",
|
||||
"dir_1": {
|
||||
"file_1.py": "# File 1_1 contents",
|
||||
"file_2.py": "# File 1_2 contents",
|
||||
"file_3.py": "# File 1_3 contents",
|
||||
"gitignored_dir": {
|
||||
"file_a.py": "# File contents",
|
||||
"file_b.py": "# File contents",
|
||||
"file_c.py": "# File contents",
|
||||
},
|
||||
},
|
||||
"dir_2": {
|
||||
"file_1.py": "# File 2_1 contents",
|
||||
"file_2.py": "# File 2_2 contents",
|
||||
"file_3.py": "# File 2_3 contents",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
]
|
||||
);
|
||||
|
||||
let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
|
||||
.expect("dir 1 file is not ignored and should have an entry");
|
||||
let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
|
||||
.expect("dir 2 file is not ignored and should have an entry");
|
||||
let gitignored_dir_file =
|
||||
find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
|
||||
assert_eq!(
|
||||
gitignored_dir_file, None,
|
||||
"File in the gitignored dir should not have an entry before its dir is toggled"
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "project_root/dir_1", cx);
|
||||
toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir <== selected",
|
||||
" file_a.py",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"Should show gitignored dir file list in the project panel"
|
||||
);
|
||||
let gitignored_dir_file =
|
||||
find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
|
||||
.expect("after gitignored dir got opened, a file entry should be present");
|
||||
|
||||
toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
|
||||
toggle_expand_dir(&panel, "project_root/dir_1", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1 <== selected",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"Should hide all dir contents again and prepare for the explicit reveal test"
|
||||
);
|
||||
|
||||
for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" > dir_1 <== selected",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
|
||||
);
|
||||
}
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" > dir_2",
|
||||
" .gitignore",
|
||||
],
|
||||
"With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" > gitignored_dir",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py <== selected",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" > .git",
|
||||
" v dir_1",
|
||||
" v gitignored_dir",
|
||||
" file_a.py <== selected",
|
||||
" file_b.py",
|
||||
" file_c.py",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" v dir_2",
|
||||
" file_1.py",
|
||||
" file_2.py",
|
||||
" file_3.py",
|
||||
" .gitignore",
|
||||
],
|
||||
"With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
|
||||
);
|
||||
}
|
||||
|
||||
fn toggle_expand_dir(
|
||||
panel: &View<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
@@ -2913,6 +3379,23 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn find_project_entry(
|
||||
panel: &View<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Option<ProjectEntryId> {
|
||||
let path = path.as_ref();
|
||||
panel.update(cx, |panel, cx| {
|
||||
for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
return worktree.entry_for_path(relative_path).map(|entry| entry.id);
|
||||
}
|
||||
}
|
||||
panic!("no worktree for path {path:?}");
|
||||
})
|
||||
}
|
||||
|
||||
fn visible_entries_as_strings(
|
||||
panel: &View<ProjectPanel>,
|
||||
range: Range<usize>,
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct ProjectPanelSettings {
|
||||
pub folder_icons: bool,
|
||||
pub git_status: bool,
|
||||
pub indent_size: f32,
|
||||
pub auto_reveal_entries: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -28,6 +29,7 @@ pub struct ProjectPanelSettingsContent {
|
||||
pub folder_icons: Option<bool>,
|
||||
pub git_status: Option<bool>,
|
||||
pub indent_size: Option<f32>,
|
||||
pub auto_reveal_entries: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for ProjectPanelSettings {
|
||||
|
||||
@@ -96,6 +96,12 @@ pub struct CloseAllItems {
|
||||
pub save_intent: Option<SaveIntent>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RevealInProjectPanel {
|
||||
pub entry_id: u64,
|
||||
}
|
||||
|
||||
actions!(
|
||||
pane,
|
||||
[
|
||||
@@ -116,7 +122,15 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]);
|
||||
impl_actions!(
|
||||
pane,
|
||||
[
|
||||
ActivateItem,
|
||||
CloseActiveItem,
|
||||
CloseAllItems,
|
||||
RevealInProjectPanel,
|
||||
]
|
||||
);
|
||||
|
||||
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
||||
|
||||
@@ -146,6 +160,13 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
|
||||
cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
|
||||
cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
|
||||
cx.add_action(|pane: &mut Pane, action: &RevealInProjectPanel, cx| {
|
||||
pane.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(
|
||||
ProjectEntryId::from_proto(action.entry_id),
|
||||
))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1232,80 +1253,87 @@ impl Pane {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let active_item_id = self.items[self.active_item_index].id();
|
||||
let single_entry_to_resolve =
|
||||
self.items()
|
||||
.find(|i| i.id() == target_item_id)
|
||||
.and_then(|i| {
|
||||
let item_entries = i.project_entry_ids(cx);
|
||||
if item_entries.len() == 1 {
|
||||
Some(item_entries[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let is_active_item = target_item_id == active_item_id;
|
||||
let target_pane = cx.weak_handle();
|
||||
|
||||
// The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
|
||||
|
||||
self.tab_context_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
position,
|
||||
AnchorCorner::TopLeft,
|
||||
if is_active_item {
|
||||
vec![
|
||||
ContextMenuItem::action(
|
||||
"Close Active Item",
|
||||
CloseActiveItem { save_intent: None },
|
||||
),
|
||||
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
|
||||
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
|
||||
ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
|
||||
ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
|
||||
ContextMenuItem::action(
|
||||
"Close All Items",
|
||||
CloseAllItems { save_intent: None },
|
||||
),
|
||||
]
|
||||
} else {
|
||||
// In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
|
||||
vec![
|
||||
ContextMenuItem::handler("Close Inactive Item", {
|
||||
let pane = target_pane.clone();
|
||||
move |cx| {
|
||||
if let Some(pane) = pane.upgrade(cx) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_item_by_id(
|
||||
target_item_id,
|
||||
SaveIntent::Close,
|
||||
cx,
|
||||
)
|
||||
let mut menu_items = if is_active_item {
|
||||
vec![
|
||||
ContextMenuItem::action(
|
||||
"Close Active Item",
|
||||
CloseActiveItem { save_intent: None },
|
||||
),
|
||||
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
|
||||
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
|
||||
ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
|
||||
ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
|
||||
ContextMenuItem::action("Close All Items", CloseAllItems { save_intent: None }),
|
||||
]
|
||||
} else {
|
||||
// In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
|
||||
vec![
|
||||
ContextMenuItem::handler("Close Inactive Item", {
|
||||
let pane = target_pane.clone();
|
||||
move |cx| {
|
||||
if let Some(pane) = pane.upgrade(cx) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_item_by_id(target_item_id, SaveIntent::Close, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
|
||||
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
|
||||
ContextMenuItem::handler("Close Items To The Left", {
|
||||
let pane = target_pane.clone();
|
||||
move |cx| {
|
||||
if let Some(pane) = pane.upgrade(cx) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_items_to_the_left_by_id(target_item_id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
}),
|
||||
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
|
||||
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
|
||||
ContextMenuItem::handler("Close Items To The Left", {
|
||||
let pane = target_pane.clone();
|
||||
move |cx| {
|
||||
if let Some(pane) = pane.upgrade(cx) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_items_to_the_left_by_id(target_item_id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
}),
|
||||
ContextMenuItem::handler("Close Items To The Right", {
|
||||
let pane = target_pane.clone();
|
||||
move |cx| {
|
||||
if let Some(pane) = pane.upgrade(cx) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_items_to_the_right_by_id(target_item_id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
}),
|
||||
ContextMenuItem::handler("Close Items To The Right", {
|
||||
let pane = target_pane.clone();
|
||||
move |cx| {
|
||||
if let Some(pane) = pane.upgrade(cx) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_items_to_the_right_by_id(target_item_id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
}),
|
||||
ContextMenuItem::action(
|
||||
"Close All Items",
|
||||
CloseAllItems { save_intent: None },
|
||||
),
|
||||
]
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
ContextMenuItem::action("Close All Items", CloseAllItems { save_intent: None }),
|
||||
]
|
||||
};
|
||||
|
||||
if let Some(entry) = single_entry_to_resolve {
|
||||
menu_items.push(ContextMenuItem::Separator);
|
||||
menu_items.push(ContextMenuItem::action(
|
||||
"Reveal In Project Panel",
|
||||
RevealInProjectPanel {
|
||||
entry_id: entry.to_proto(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
menu.show(position, AnchorCorner::TopLeft, menu_items, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,21 @@ pub struct CloseAllItems {
|
||||
pub save_intent: Option<SaveIntent>,
|
||||
}
|
||||
|
||||
impl_actions!(pane, [CloseAllItems, CloseActiveItem, ActivateItem]);
|
||||
#[derive(Clone, PartialEq, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RevealInProjectPanel {
|
||||
pub entry_id: u64,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
pane,
|
||||
[
|
||||
CloseAllItems,
|
||||
CloseActiveItem,
|
||||
ActivateItem,
|
||||
RevealInProjectPanel
|
||||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
pane,
|
||||
@@ -1499,9 +1513,19 @@ impl Pane {
|
||||
)
|
||||
.child(label);
|
||||
|
||||
right_click_menu(ix).trigger(tab).menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, cx| {
|
||||
menu.action("Close", CloseActiveItem { save_intent: None }.boxed_clone())
|
||||
let single_entry_to_resolve = {
|
||||
let item_entries = self.items[ix].project_entry_ids(cx);
|
||||
if item_entries.len() == 1 {
|
||||
Some(item_entries[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
right_click_menu(ix).trigger(tab).menu(move |cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
let menu = menu
|
||||
.action("Close", CloseActiveItem { save_intent: None }.boxed_clone())
|
||||
.action("Close Others", CloseInactiveItems.boxed_clone())
|
||||
.separator()
|
||||
.action("Close Left", CloseItemsToTheLeft.boxed_clone())
|
||||
@@ -1511,7 +1535,19 @@ impl Pane {
|
||||
.action(
|
||||
"Close All",
|
||||
CloseAllItems { save_intent: None }.boxed_clone(),
|
||||
);
|
||||
|
||||
if let Some(entry) = single_entry_to_resolve {
|
||||
menu.separator().action(
|
||||
"Reveal In Project Panel",
|
||||
RevealInProjectPanel {
|
||||
entry_id: entry.to_proto(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
} else {
|
||||
menu
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -2135,6 +2171,15 @@ impl Render for Pane {
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
|
||||
pane.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(
|
||||
ProjectEntryId::from_proto(action.entry_id),
|
||||
))
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(self.render_tab_bar(cx))
|
||||
.child(self.toolbar.clone())
|
||||
.child(if let Some(item) = self.active_item() {
|
||||
|
||||
@@ -130,7 +130,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
let vim_mode_indicator = cx.build_view(|cx| vim::ModeIndicator::new(cx));
|
||||
let feedback_button = cx
|
||||
.build_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
|
||||
// let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
|
||||
let cursor_position = cx.build_view(|_| editor::items::CursorPosition::new());
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
status_bar.add_left_item(diagnostic_summary, cx);
|
||||
|
||||
Reference in New Issue
Block a user