Skip to repository content295 lines · 9.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:02:25.261Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
multi_select_tests.rs
1//! Tests for multi-selecting files in the file finder: toggling items in and
2//! out of the selection, pinning the selection to the top across query
3//! changes, and the various ways of opening the whole selection.
4
5use gpui::{BorrowAppContext, Entity, TestAppContext, VisualTestContext};
6use picker::{MultiSelectNext, Picker, PickerDelegate as _};
7use pretty_assertions::assert_eq;
8use project::Project;
9use serde_json::json;
10use settings::SettingsStore;
11use util::path;
12use workspace::{MultiWorkspace, Workspace, pane};
13
14use crate::file_finder_tests::{self, open_file_picker};
15use crate::{FileFinder, FileFinderDelegate, SEARCH_DEBOUNCE};
16
17struct TestContext {
18 picker: Entity<Picker<FileFinderDelegate>>,
19 workspace: Entity<Workspace>,
20 cx: VisualTestContext,
21}
22
23impl TestContext {
24 /// The test tree is `a.rs`, `b.rs` and `c.rs` in the worktree root.
25 /// Preview tabs from the file finder are enabled to verify that batch
26 /// opens produce real tabs regardless.
27 async fn new(cx: &mut TestAppContext) -> TestContext {
28 let app_state = file_finder_tests::init_test(cx);
29 cx.update(|cx| {
30 cx.update_global::<SettingsStore, _>(|store, cx| {
31 store.update_user_settings(cx, |settings| {
32 settings
33 .preview_tabs
34 .get_or_insert_default()
35 .enable_preview_from_file_finder = Some(true);
36 });
37 })
38 });
39 app_state
40 .fs
41 .as_fake()
42 .insert_tree(
43 path!("/root"),
44 json!({
45 "a.rs": "// a",
46 "b.rs": "// b",
47 "c.rs": "// c",
48 }),
49 )
50 .await;
51 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
52 let window =
53 cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
54 let workspace = window
55 .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
56 .unwrap();
57 let mut cx = VisualTestContext::from_window(window.into(), cx);
58 let picker = open_file_picker(&workspace, &mut cx);
59 TestContext {
60 picker,
61 workspace,
62 cx,
63 }
64 }
65
66 fn reopen_finder(&mut self) {
67 self.picker = open_file_picker(&self.workspace, &mut self.cx);
68 }
69
70 fn search(&mut self, query: &str) {
71 self.picker.update_in(&mut self.cx, |picker, window, cx| {
72 picker.set_query(query, window, cx)
73 });
74 self.cx.executor().advance_clock(SEARCH_DEBOUNCE);
75 self.cx.run_until_parked();
76 }
77
78 fn select(&mut self, file_name: &str) {
79 let index = self.index_of(file_name);
80 self.picker.update_in(&mut self.cx, |picker, window, cx| {
81 picker.delegate.set_selected_index(index, window, cx);
82 });
83 self.cx.dispatch_action(MultiSelectNext);
84 self.cx.run_until_parked();
85 }
86
87 fn deselect(&mut self, file_name: &str) {
88 let index = self.index_of(file_name);
89 self.picker.update_in(&mut self.cx, |picker, window, cx| {
90 picker.delegate.toggle_item_selected(index, window, cx);
91 });
92 self.cx.run_until_parked();
93 }
94
95 fn confirm(&mut self) {
96 self.cx.dispatch_action(menu::Confirm);
97 self.cx.run_until_parked();
98 }
99
100 fn secondary_confirm(&mut self) {
101 self.cx.dispatch_action(menu::SecondaryConfirm);
102 self.cx.run_until_parked();
103 }
104
105 fn split_right(&mut self) {
106 self.cx.dispatch_action(pane::SplitRight::default());
107 self.cx.run_until_parked();
108 }
109
110 fn index_of(&mut self, file_name: &str) -> usize {
111 self.picker.update(&mut self.cx, |picker, _| {
112 picker
113 .delegate
114 .matches
115 .matches
116 .iter()
117 .position(|m| {
118 m.relative_path()
119 .is_some_and(|path| path.file_name() == Some(file_name))
120 })
121 .unwrap_or_else(|| panic!("{file_name} is not in the match list"))
122 })
123 }
124
125 fn match_names(&mut self) -> Vec<String> {
126 self.picker.update(&mut self.cx, |picker, _| {
127 picker
128 .delegate
129 .matches
130 .matches
131 .iter()
132 .filter_map(|m| Some(m.relative_path()?.file_name()?.to_owned()))
133 .collect()
134 })
135 }
136
137 #[track_caller]
138 fn assert_selected(&mut self, expected: &[&str]) {
139 let selected: Vec<String> = self.picker.update(&mut self.cx, |picker, _| {
140 picker
141 .delegate
142 .selected_matches
143 .iter()
144 .filter_map(|selected| Some(selected.0.relative_path()?.file_name()?.to_owned()))
145 .collect()
146 });
147 assert_eq!(selected, expected, "wrong files selected");
148 }
149
150 #[track_caller]
151 fn assert_finder_closed(&mut self) {
152 let finder_open = self.workspace.update(&mut self.cx, |workspace, cx| {
153 workspace.active_modal::<FileFinder>(cx).is_some()
154 });
155 assert!(!finder_open, "the finder should have been dismissed");
156 }
157
158 #[track_caller]
159 fn assert_active_pane_items(&mut self, expected: &[&str]) {
160 let mut items = self.pane_item_names(
161 &self
162 .workspace
163 .read_with(&self.cx, |workspace, _| workspace.active_pane().clone()),
164 );
165 let mut expected: Vec<String> = expected.iter().map(|name| name.to_string()).collect();
166 items.sort();
167 expected.sort();
168 assert_eq!(items, expected, "wrong items in the active pane");
169 }
170
171 fn pane_item_names(&mut self, pane: &Entity<pane::Pane>) -> Vec<String> {
172 pane.read_with(&self.cx, |pane, cx| {
173 pane.items()
174 .filter_map(|item| Some(item.project_path(cx)?.path.file_name()?.to_owned()))
175 .collect()
176 })
177 }
178
179 fn pane_count(&mut self) -> usize {
180 self.workspace
181 .read_with(&self.cx, |workspace, _| workspace.panes().len())
182 }
183}
184
185#[gpui::test]
186async fn open_selection_as_tabs(cx: &mut TestAppContext) {
187 let mut cx = TestContext::new(cx).await;
188
189 cx.search("rs");
190 cx.select("b.rs");
191 cx.select("c.rs");
192 cx.assert_selected(&["b.rs", "c.rs"]);
193
194 cx.confirm();
195 cx.assert_finder_closed();
196 // Both files must open as real tabs even though preview tabs from the
197 // file finder are enabled; a batch open must not reuse one preview tab.
198 cx.assert_active_pane_items(&["b.rs", "c.rs"]);
199}
200
201#[gpui::test]
202async fn tabbing_a_selected_row_deselects_it(cx: &mut TestAppContext) {
203 let mut cx = TestContext::new(cx).await;
204
205 cx.search("rs");
206 cx.select("b.rs");
207 cx.select("c.rs");
208 cx.select("b.rs");
209 cx.assert_selected(&["c.rs"]);
210}
211
212#[gpui::test]
213async fn selection_pins_to_top_across_queries(cx: &mut TestAppContext) {
214 let mut cx = TestContext::new(cx).await;
215
216 cx.search("c");
217 cx.select("c.rs");
218
219 // `c.rs` doesn't match the new query, but stays selected and pinned to
220 // the top of the results.
221 cx.search("b");
222 cx.assert_selected(&["c.rs"]);
223 assert_eq!(cx.match_names(), ["c.rs", "b.rs"]);
224}
225
226#[gpui::test]
227async fn deselecting_survives_queries(cx: &mut TestAppContext) {
228 let mut cx = TestContext::new(cx).await;
229
230 cx.search("c");
231 cx.select("c.rs");
232
233 cx.search("b");
234 cx.deselect("c.rs");
235 cx.assert_selected(&[]);
236
237 // A deselected file must not come back selected or pinned on requery.
238 cx.search("a");
239 cx.assert_selected(&[]);
240 assert_eq!(cx.match_names(), ["a.rs"]);
241}
242
243#[gpui::test]
244async fn create_new_file_row_is_not_selectable(cx: &mut TestAppContext) {
245 let mut cx = TestContext::new(cx).await;
246
247 // A query matching nothing produces only the "create new" row.
248 cx.search("zzz");
249 cx.picker.update_in(&mut cx.cx, |picker, window, cx| {
250 picker.delegate.set_selected_index(0, window, cx);
251 });
252 cx.cx.dispatch_action(MultiSelectNext);
253 cx.cx.run_until_parked();
254 cx.assert_selected(&[]);
255}
256
257#[gpui::test]
258async fn open_selection_in_one_split(cx: &mut TestAppContext) {
259 let mut cx = TestContext::new(cx).await;
260
261 cx.search("rs");
262 cx.select("b.rs");
263 cx.select("c.rs");
264
265 cx.split_right();
266 cx.assert_finder_closed();
267 // One new pane holding both files as tabs, not one pane per file.
268 assert_eq!(cx.pane_count(), 2);
269 cx.assert_active_pane_items(&["b.rs", "c.rs"]);
270}
271
272#[gpui::test]
273async fn secondary_confirm_opens_one_split_per_file(cx: &mut TestAppContext) {
274 let mut cx = TestContext::new(cx).await;
275
276 // Open a file normally first so the workspace has a non-empty pane to
277 // split off from.
278 cx.search("a");
279 cx.confirm();
280 cx.assert_active_pane_items(&["a.rs"]);
281
282 cx.reopen_finder();
283 cx.search("rs");
284 cx.select("b.rs");
285 cx.select("c.rs");
286
287 cx.secondary_confirm();
288 cx.assert_finder_closed();
289 assert_eq!(
290 cx.pane_count(),
291 3,
292 "each selected file opens in its own split"
293 );
294}
295