mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-20 02:47:34 +03:00
Add search in directory action in the project panel
This commit is contained in:
parent
e002d9efb0
commit
595bc16749
@ -529,7 +529,8 @@
|
|||||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||||
"f2": "project_panel::Rename",
|
"f2": "project_panel::Rename",
|
||||||
"backspace": "project_panel::Delete",
|
"backspace": "project_panel::Delete",
|
||||||
"alt-cmd-r": "project_panel::RevealInFinder"
|
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||||
|
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -125,7 +125,8 @@ actions!(
|
|||||||
Paste,
|
Paste,
|
||||||
Delete,
|
Delete,
|
||||||
Rename,
|
Rename,
|
||||||
ToggleFocus
|
ToggleFocus,
|
||||||
|
NewSearchInDirectory,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -151,6 +152,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
|
|||||||
cx.add_action(ProjectPanel::copy_path);
|
cx.add_action(ProjectPanel::copy_path);
|
||||||
cx.add_action(ProjectPanel::copy_relative_path);
|
cx.add_action(ProjectPanel::copy_relative_path);
|
||||||
cx.add_action(ProjectPanel::reveal_in_finder);
|
cx.add_action(ProjectPanel::reveal_in_finder);
|
||||||
|
cx.add_action(ProjectPanel::new_search_in_directory);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
|
|this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
|
||||||
this.paste(action, cx);
|
this.paste(action, cx);
|
||||||
@ -169,6 +171,9 @@ pub enum Event {
|
|||||||
},
|
},
|
||||||
DockPositionChanged,
|
DockPositionChanged,
|
||||||
Focus,
|
Focus,
|
||||||
|
NewSearchInDirectory {
|
||||||
|
dir_entry: Entry,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -417,6 +422,12 @@ impl ProjectPanel {
|
|||||||
CopyRelativePath,
|
CopyRelativePath,
|
||||||
));
|
));
|
||||||
menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
|
menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
|
||||||
|
if entry.is_dir() {
|
||||||
|
menu_entries.push(ContextMenuItem::action(
|
||||||
|
"Search inside",
|
||||||
|
NewSearchInDirectory,
|
||||||
|
));
|
||||||
|
}
|
||||||
if let Some(clipboard_entry) = self.clipboard_entry {
|
if let Some(clipboard_entry) = self.clipboard_entry {
|
||||||
if clipboard_entry.worktree_id() == worktree.id() {
|
if clipboard_entry.worktree_id() == worktree.id() {
|
||||||
menu_entries.push(ContextMenuItem::action("Paste", Paste));
|
menu_entries.push(ContextMenuItem::action("Paste", Paste));
|
||||||
@ -928,6 +939,20 @@ impl ProjectPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_search_in_directory(
|
||||||
|
&mut self,
|
||||||
|
_: &NewSearchInDirectory,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if let Some((_, entry)) = self.selected_entry(cx) {
|
||||||
|
if entry.is_dir() {
|
||||||
|
cx.emit(Event::NewSearchInDirectory {
|
||||||
|
dir_entry: entry.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn move_entry(
|
fn move_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry_to_move: ProjectEntryId,
|
entry_to_move: ProjectEntryId,
|
||||||
@ -1677,7 +1702,11 @@ mod tests {
|
|||||||
use project::FakeFs;
|
use project::FakeFs;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{collections::HashSet, path::Path};
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
path::Path,
|
||||||
|
sync::atomic::{self, AtomicUsize},
|
||||||
|
};
|
||||||
use workspace::{pane, AppState};
|
use workspace::{pane, AppState};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@ -2516,6 +2545,83 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test_with_editor(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/src",
|
||||||
|
json!({
|
||||||
|
"test": {
|
||||||
|
"first.rs": "// First Rust file",
|
||||||
|
"second.rs": "// Second Rust file",
|
||||||
|
"third.rs": "// Third Rust file",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||||
|
|
||||||
|
let new_search_events_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
let _subscription = panel.update(cx, |_, cx| {
|
||||||
|
let subcription_count = Arc::clone(&new_search_events_count);
|
||||||
|
cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||||
|
if matches!(event, Event::NewSearchInDirectory { .. }) {
|
||||||
|
subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
toggle_expand_dir(&panel, "src/test", cx);
|
||||||
|
select_path(&panel, "src/test/first.rs", cx);
|
||||||
|
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..10, cx),
|
||||||
|
&[
|
||||||
|
"v src",
|
||||||
|
" v test",
|
||||||
|
" first.rs <== selected",
|
||||||
|
" second.rs",
|
||||||
|
" third.rs"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.new_search_in_directory(&NewSearchInDirectory, cx)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
new_search_events_count.load(atomic::Ordering::SeqCst),
|
||||||
|
0,
|
||||||
|
"Should not trigger new search in directory when called on a file"
|
||||||
|
);
|
||||||
|
|
||||||
|
select_path(&panel, "src/test", cx);
|
||||||
|
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..10, cx),
|
||||||
|
&[
|
||||||
|
"v src",
|
||||||
|
" v test <== selected",
|
||||||
|
" first.rs",
|
||||||
|
" second.rs",
|
||||||
|
" third.rs"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.new_search_in_directory(&NewSearchInDirectory, cx)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
new_search_events_count.load(atomic::Ordering::SeqCst),
|
||||||
|
1,
|
||||||
|
"Should trigger new search in directory when called on a directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_expand_dir(
|
fn toggle_expand_dir(
|
||||||
panel: &ViewHandle<ProjectPanel>,
|
panel: &ViewHandle<ProjectPanel>,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
|
@ -18,7 +18,7 @@ use gpui::{
|
|||||||
Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
|
Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use menu::Confirm;
|
use menu::Confirm;
|
||||||
use project::{search::SearchQuery, Project};
|
use project::{search::SearchQuery, Entry, Project};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
@ -501,6 +501,28 @@ impl ProjectSearchView {
|
|||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_search_in_directory(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
dir_entry: &Entry,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
if !dir_entry.is_dir() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let filter_path = dir_entry.path.join("**");
|
||||||
|
let Some(filter_str) = filter_path.to_str() else { return; };
|
||||||
|
|
||||||
|
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
|
||||||
|
let search = cx.add_view(|cx| ProjectSearchView::new(model, cx));
|
||||||
|
workspace.add_item(Box::new(search.clone()), cx);
|
||||||
|
search.update(cx, |search, cx| {
|
||||||
|
search
|
||||||
|
.included_files_editor
|
||||||
|
.update(cx, |editor, cx| editor.set_text(filter_str, cx));
|
||||||
|
search.focus_query_editor(cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Re-activate the most recently activated search or the most recent if it has been closed.
|
// Re-activate the most recently activated search or the most recent if it has been closed.
|
||||||
// If no search exists in the workspace, create a new one.
|
// If no search exists in the workspace, create a new one.
|
||||||
fn deploy(
|
fn deploy(
|
||||||
@ -1414,6 +1436,134 @@ pub mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_new_project_search_in_directory(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"a": {
|
||||||
|
"one.rs": "const ONE: usize = 1;",
|
||||||
|
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
|
||||||
|
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
|
let worktree_id = project.read_with(cx, |project, cx| {
|
||||||
|
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||||
|
});
|
||||||
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
|
|
||||||
|
let active_item = cx.read(|cx| {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.active_item()
|
||||||
|
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
active_item.is_none(),
|
||||||
|
"Expected no search panel to be active, but got: {active_item:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let one_file_entry = cx.update(|cx| {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.entry_for_path(&(worktree_id, "a/one.rs").into(), cx)
|
||||||
|
.expect("no entry for /a/one.rs file")
|
||||||
|
});
|
||||||
|
assert!(one_file_entry.is_file());
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx)
|
||||||
|
});
|
||||||
|
let active_search_entry = cx.read(|cx| {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.active_item()
|
||||||
|
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
active_search_entry.is_none(),
|
||||||
|
"Expected no search panel to be active for file entry"
|
||||||
|
);
|
||||||
|
|
||||||
|
let a_dir_entry = cx.update(|cx| {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.entry_for_path(&(worktree_id, "a").into(), cx)
|
||||||
|
.expect("no entry for /a/ directory")
|
||||||
|
});
|
||||||
|
assert!(a_dir_entry.is_dir());
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some(search_view) = cx.read(|cx| {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.active_item()
|
||||||
|
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||||
|
}) else {
|
||||||
|
panic!("Search view expected to appear after new search in directory event trigger")
|
||||||
|
};
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
assert!(
|
||||||
|
search_view.query_editor.is_focused(cx),
|
||||||
|
"On new search in directory, focus should be moved into query editor"
|
||||||
|
);
|
||||||
|
search_view.excluded_files_editor.update(cx, |editor, cx| {
|
||||||
|
assert!(
|
||||||
|
editor.display_text(cx).is_empty(),
|
||||||
|
"New search in directory should not have any excluded files"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
search_view.included_files_editor.update(cx, |editor, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
editor.display_text(cx),
|
||||||
|
a_dir_entry.path.join("**").display().to_string(),
|
||||||
|
"New search in directory should have included dir entry path"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
search_view
|
||||||
|
.query_editor
|
||||||
|
.update(cx, |query_editor, cx| query_editor.set_text("const", cx));
|
||||||
|
search_view.search(cx);
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
search_view
|
||||||
|
.results_editor
|
||||||
|
.update(cx, |editor, cx| editor.display_text(cx)),
|
||||||
|
"\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
|
||||||
|
"New search in directory should have a filter that matches a certain directory"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_test(cx: &mut TestAppContext) {
|
pub fn init_test(cx: &mut TestAppContext) {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
let fonts = cx.font_cache();
|
let fonts = cx.font_cache();
|
||||||
|
@ -512,7 +512,7 @@ pub struct Workspace {
|
|||||||
follower_states_by_leader: FollowerStatesByLeader,
|
follower_states_by_leader: FollowerStatesByLeader,
|
||||||
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
||||||
window_edited: bool,
|
window_edited: bool,
|
||||||
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
|
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
|
||||||
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
||||||
database_id: WorkspaceId,
|
database_id: WorkspaceId,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
@ -3009,6 +3009,10 @@ impl Workspace {
|
|||||||
self.database_id
|
self.database_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push_subscription(&mut self, subscription: Subscription) {
|
||||||
|
self.subscriptions.push(subscription)
|
||||||
|
}
|
||||||
|
|
||||||
fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
|
fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
|
||||||
let project = self.project().read(cx);
|
let project = self.project().read(cx);
|
||||||
|
|
||||||
|
@ -338,6 +338,27 @@ pub fn initialize_workspace(
|
|||||||
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let (project_panel, terminal_panel, assistant_panel) =
|
let (project_panel, terminal_panel, assistant_panel) =
|
||||||
futures::try_join!(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| {
|
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||||
let project_panel_position = project_panel.position(cx);
|
let project_panel_position = project_panel.position(cx);
|
||||||
workspace.add_panel(project_panel, cx);
|
workspace.add_panel(project_panel, cx);
|
||||||
|
Loading…
Reference in New Issue
Block a user