Port to gpui2

This commit is contained in:
Kirill Bulatov 2023-12-12 13:50:23 +02:00
parent 721d7615c3
commit d9b0828beb
4 changed files with 542 additions and 11 deletions

View File

@ -299,6 +299,7 @@ pub enum Event {
CollaboratorJoined(proto::PeerId),
CollaboratorLeft(proto::PeerId),
RefreshInlayHints,
RevealInProjectPanel(ProjectEntryId),
}
pub enum LanguageServerState {

View File

@ -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>,

View File

@ -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 {

View File

@ -1,4 +1,4 @@
se crate::{
use crate::{
item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
toolbar::Toolbar,
workspace_settings::{AutosaveSetting, WorkspaceSettings},
@ -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() {