1pub mod pane;
2pub mod pane_group;
3pub use pane::*;
4pub use pane_group::*;
5
6use crate::{
7 settings::Settings,
8 watch::{self, Receiver},
9 worktree::FileHandle,
10};
11use gpui::{MutableAppContext, PathPromptOptions};
12use std::path::PathBuf;
13pub fn init(app: &mut MutableAppContext) {
14 app.add_global_action("workspace:open", open);
15 app.add_global_action("workspace:open_paths", open_paths);
16 app.add_global_action("app:quit", quit);
17 app.add_action("workspace:save", Workspace::save_active_item);
18 app.add_action("workspace:debug_elements", Workspace::debug_elements);
19 app.add_action("workspace:new_file", Workspace::open_new_file);
20 app.add_bindings(vec![
21 Binding::new("cmd-s", "workspace:save", None),
22 Binding::new("cmd-alt-i", "workspace:debug_elements", None),
23 ]);
24 pane::init(app);
25}
26use crate::{
27 editor::{Buffer, BufferView},
28 time::ReplicaId,
29 worktree::{Worktree, WorktreeHandle},
30};
31use futures_core::{future::LocalBoxFuture, Future};
32use gpui::{
33 color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
34 ClipboardItem, Entity, EntityTask, ModelHandle, View, ViewContext, ViewHandle,
35};
36use log::error;
37use smol::prelude::*;
38use std::{
39 collections::{hash_map::Entry, HashMap, HashSet},
40 path::Path,
41 sync::Arc,
42};
43
44pub struct OpenParams {
45 pub paths: Vec<PathBuf>,
46 pub settings: watch::Receiver<Settings>,
47}
48
49fn open(settings: &Receiver<Settings>, ctx: &mut MutableAppContext) {
50 let settings = settings.clone();
51 ctx.prompt_for_paths(
52 PathPromptOptions {
53 files: true,
54 directories: true,
55 multiple: true,
56 },
57 move |paths, ctx| {
58 if let Some(paths) = paths {
59 ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, settings });
60 }
61 },
62 );
63}
64
65fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
66 log::info!("open paths {:?}", params.paths);
67
68 // Open paths in existing workspace if possible
69 for window_id in app.window_ids().collect::<Vec<_>>() {
70 if let Some(handle) = app.root_view::<Workspace>(window_id) {
71 if handle.update(app, |view, ctx| {
72 if view.contains_paths(¶ms.paths, ctx.as_ref()) {
73 let open_paths = view.open_paths(¶ms.paths, ctx);
74 ctx.foreground().spawn(open_paths).detach();
75 log::info!("open paths on existing workspace");
76 true
77 } else {
78 false
79 }
80 }) {
81 return;
82 }
83 }
84 }
85
86 log::info!("open new workspace");
87
88 // Add a new workspace if necessary
89 app.add_window(|ctx| {
90 let mut view = Workspace::new(0, params.settings.clone(), ctx);
91 let open_paths = view.open_paths(¶ms.paths, ctx);
92 ctx.foreground().spawn(open_paths).detach();
93 view
94 });
95}
96
97fn quit(_: &(), app: &mut MutableAppContext) {
98 app.platform().quit();
99}
100
101pub trait ItemView: View {
102 fn title(&self, app: &AppContext) -> String;
103 fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)>;
104 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
105 where
106 Self: Sized,
107 {
108 None
109 }
110 fn is_dirty(&self, _: &AppContext) -> bool {
111 false
112 }
113 fn save(
114 &mut self,
115 _: Option<FileHandle>,
116 _: &mut ViewContext<Self>,
117 ) -> LocalBoxFuture<'static, anyhow::Result<()>> {
118 Box::pin(async { Ok(()) })
119 }
120 fn should_activate_item_on_event(_: &Self::Event) -> bool {
121 false
122 }
123 fn should_update_tab_on_event(_: &Self::Event) -> bool {
124 false
125 }
126}
127
128pub trait ItemViewHandle: Send + Sync {
129 fn title(&self, app: &AppContext) -> String;
130 fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)>;
131 fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
132 fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
133 fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
134 fn id(&self) -> usize;
135 fn to_any(&self) -> AnyViewHandle;
136 fn is_dirty(&self, ctx: &AppContext) -> bool;
137 fn save(
138 &self,
139 file: Option<FileHandle>,
140 ctx: &mut MutableAppContext,
141 ) -> LocalBoxFuture<'static, anyhow::Result<()>>;
142}
143
144impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
145 fn title(&self, app: &AppContext) -> String {
146 self.read(app).title(app)
147 }
148
149 fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)> {
150 self.read(app).entry_id(app)
151 }
152
153 fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
154 Box::new(self.clone())
155 }
156
157 fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
158 self.update(app, |item, ctx| {
159 ctx.add_option_view(|ctx| item.clone_on_split(ctx))
160 })
161 .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
162 }
163
164 fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext) {
165 pane.update(app, |_, ctx| {
166 ctx.subscribe_to_view(self, |pane, item, event, ctx| {
167 if T::should_activate_item_on_event(event) {
168 if let Some(ix) = pane.item_index(&item) {
169 pane.activate_item(ix, ctx);
170 pane.activate(ctx);
171 }
172 }
173 if T::should_update_tab_on_event(event) {
174 ctx.notify()
175 }
176 })
177 })
178 }
179
180 fn save(
181 &self,
182 file: Option<FileHandle>,
183 ctx: &mut MutableAppContext,
184 ) -> LocalBoxFuture<'static, anyhow::Result<()>> {
185 self.update(ctx, |item, ctx| item.save(file, ctx))
186 }
187
188 fn is_dirty(&self, ctx: &AppContext) -> bool {
189 self.read(ctx).is_dirty(ctx)
190 }
191
192 fn id(&self) -> usize {
193 self.id()
194 }
195
196 fn to_any(&self) -> AnyViewHandle {
197 self.into()
198 }
199}
200
201impl Clone for Box<dyn ItemViewHandle> {
202 fn clone(&self) -> Box<dyn ItemViewHandle> {
203 self.boxed_clone()
204 }
205}
206
207#[derive(Debug)]
208pub struct State {
209 pub modal: Option<usize>,
210 pub center: PaneGroup,
211}
212
213pub struct Workspace {
214 pub settings: watch::Receiver<Settings>,
215 modal: Option<AnyViewHandle>,
216 center: PaneGroup,
217 panes: Vec<ViewHandle<Pane>>,
218 active_pane: ViewHandle<Pane>,
219 loading_entries: HashSet<(usize, Arc<Path>)>,
220 replica_id: ReplicaId,
221 worktrees: HashSet<ModelHandle<Worktree>>,
222 buffers: HashMap<
223 (usize, u64),
224 postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
225 >,
226 untitled_buffers: HashSet<ModelHandle<Buffer>>,
227}
228
229impl Workspace {
230 pub fn new(
231 replica_id: ReplicaId,
232 settings: watch::Receiver<Settings>,
233 ctx: &mut ViewContext<Self>,
234 ) -> Self {
235 let pane = ctx.add_view(|_| Pane::new(settings.clone()));
236 let pane_id = pane.id();
237 ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
238 me.handle_pane_event(pane_id, event, ctx)
239 });
240 ctx.focus(&pane);
241
242 Workspace {
243 modal: None,
244 center: PaneGroup::new(pane.id()),
245 panes: vec![pane.clone()],
246 active_pane: pane.clone(),
247 loading_entries: HashSet::new(),
248 settings,
249 replica_id,
250 worktrees: Default::default(),
251 buffers: Default::default(),
252 untitled_buffers: Default::default(),
253 }
254 }
255
256 pub fn worktrees(&self) -> &HashSet<ModelHandle<Worktree>> {
257 &self.worktrees
258 }
259
260 pub fn contains_paths(&self, paths: &[PathBuf], app: &AppContext) -> bool {
261 paths.iter().all(|path| self.contains_path(&path, app))
262 }
263
264 pub fn contains_path(&self, path: &Path, app: &AppContext) -> bool {
265 self.worktrees
266 .iter()
267 .any(|worktree| worktree.read(app).contains_abs_path(path))
268 }
269
270 pub fn worktree_scans_complete(&self, ctx: &AppContext) -> impl Future<Output = ()> + 'static {
271 let futures = self
272 .worktrees
273 .iter()
274 .map(|worktree| worktree.read(ctx).scan_complete())
275 .collect::<Vec<_>>();
276 async move {
277 for future in futures {
278 future.await;
279 }
280 }
281 }
282
283 pub fn open_paths(
284 &mut self,
285 paths: &[PathBuf],
286 ctx: &mut ViewContext<Self>,
287 ) -> impl Future<Output = ()> {
288 let entries = paths
289 .iter()
290 .cloned()
291 .map(|path| self.file_for_path(&path, ctx))
292 .collect::<Vec<_>>();
293
294 let bg = ctx.background_executor().clone();
295 let tasks = paths
296 .iter()
297 .cloned()
298 .zip(entries.into_iter())
299 .map(|(abs_path, file)| {
300 ctx.spawn(
301 bg.spawn(async move { abs_path.is_file() }),
302 move |me, is_file, ctx| {
303 if is_file {
304 me.open_entry(file.entry_id(), ctx)
305 } else {
306 None
307 }
308 },
309 )
310 })
311 .collect::<Vec<_>>();
312 async move {
313 for task in tasks {
314 if let Some(task) = task.await {
315 task.await;
316 }
317 }
318 }
319 }
320
321 fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext<Self>) -> FileHandle {
322 for tree in self.worktrees.iter() {
323 if let Ok(relative_path) = abs_path.strip_prefix(tree.read(ctx).abs_path()) {
324 return tree.file(relative_path, ctx.as_ref());
325 }
326 }
327 let worktree = self.add_worktree(&abs_path, ctx);
328 worktree.file(Path::new(""), ctx.as_ref())
329 }
330
331 pub fn add_worktree(
332 &mut self,
333 path: &Path,
334 ctx: &mut ViewContext<Self>,
335 ) -> ModelHandle<Worktree> {
336 let worktree = ctx.add_model(|ctx| Worktree::new(path, ctx));
337 ctx.observe_model(&worktree, |_, _, ctx| ctx.notify());
338 self.worktrees.insert(worktree.clone());
339 ctx.notify();
340 worktree
341 }
342
343 pub fn toggle_modal<V, F>(&mut self, ctx: &mut ViewContext<Self>, add_view: F)
344 where
345 V: 'static + View,
346 F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
347 {
348 if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
349 self.modal.take();
350 ctx.focus_self();
351 } else {
352 let modal = add_view(ctx, self);
353 ctx.focus(&modal);
354 self.modal = Some(modal.into());
355 }
356 ctx.notify();
357 }
358
359 pub fn modal(&self) -> Option<&AnyViewHandle> {
360 self.modal.as_ref()
361 }
362
363 pub fn dismiss_modal(&mut self, ctx: &mut ViewContext<Self>) {
364 if self.modal.take().is_some() {
365 ctx.focus(&self.active_pane);
366 ctx.notify();
367 }
368 }
369
370 pub fn open_new_file(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
371 let buffer = ctx.add_model(|_| Buffer::new(self.replica_id, ""));
372 let buffer_view = Box::new(ctx.add_view(|ctx| {
373 BufferView::for_buffer(buffer.clone(), None, self.settings.clone(), ctx)
374 }));
375 self.untitled_buffers.insert(buffer);
376 self.add_item(buffer_view, ctx);
377 }
378
379 #[must_use]
380 pub fn open_entry(
381 &mut self,
382 entry: (usize, Arc<Path>),
383 ctx: &mut ViewContext<Self>,
384 ) -> Option<EntityTask<()>> {
385 if self.loading_entries.contains(&entry) {
386 return None;
387 }
388
389 if self
390 .active_pane()
391 .update(ctx, |pane, ctx| pane.activate_entry(entry.clone(), ctx))
392 {
393 return None;
394 }
395
396 let (worktree_id, path) = entry.clone();
397
398 let worktree = match self.worktrees.get(&worktree_id).cloned() {
399 Some(worktree) => worktree,
400 None => {
401 log::error!("worktree {} does not exist", worktree_id);
402 return None;
403 }
404 };
405
406 let inode = match worktree.read(ctx).inode_for_path(&path) {
407 Some(inode) => inode,
408 None => {
409 log::error!("path {:?} does not exist", path);
410 return None;
411 }
412 };
413
414 let file = worktree.file(path.clone(), ctx.as_ref());
415 if file.is_deleted() {
416 log::error!("path {:?} does not exist", path);
417 return None;
418 }
419
420 self.loading_entries.insert(entry.clone());
421
422 if let Entry::Vacant(entry) = self.buffers.entry((worktree_id, inode)) {
423 let (mut tx, rx) = postage::watch::channel();
424 entry.insert(rx);
425 let history = file.load_history(ctx.as_ref());
426 let replica_id = self.replica_id;
427 let buffer = ctx
428 .background_executor()
429 .spawn(async move { Ok(Buffer::from_history(replica_id, history.await?)) });
430 ctx.spawn(buffer, move |_, from_history_result, ctx| {
431 *tx.borrow_mut() = Some(match from_history_result {
432 Ok(buffer) => Ok(ctx.add_model(|_| buffer)),
433 Err(error) => Err(Arc::new(error)),
434 })
435 })
436 .detach()
437 }
438
439 let mut watch = self.buffers.get(&(worktree_id, inode)).unwrap().clone();
440 Some(ctx.spawn(
441 async move {
442 loop {
443 if let Some(load_result) = watch.borrow().as_ref() {
444 return load_result.clone();
445 }
446 watch.next().await;
447 }
448 },
449 move |me, load_result, ctx| {
450 me.loading_entries.remove(&entry);
451 match load_result {
452 Ok(buffer_handle) => {
453 let buffer_view = Box::new(ctx.add_view(|ctx| {
454 BufferView::for_buffer(
455 buffer_handle,
456 Some(file),
457 me.settings.clone(),
458 ctx,
459 )
460 }));
461 me.add_item(buffer_view, ctx);
462 }
463 Err(error) => {
464 log::error!("error opening item: {}", error);
465 }
466 }
467 },
468 ))
469 }
470
471 pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
472 let handle = ctx.handle();
473 let first_worktree = self.worktrees.iter().next();
474 self.active_pane.update(ctx, move |pane, ctx| {
475 if let Some(item) = pane.active_item() {
476 if item.entry_id(ctx.as_ref()).is_none() {
477 let start_path = first_worktree
478 .map_or(Path::new(""), |h| h.read(ctx).abs_path())
479 .to_path_buf();
480 ctx.prompt_for_new_path(&start_path, move |path, ctx| {
481 if let Some(path) = path {
482 handle.update(ctx, move |this, ctx| {
483 let file = this.file_for_path(&path, ctx);
484 let task = item.save(Some(file), ctx.as_mut());
485 ctx.spawn(task, |_, result, _| {
486 if let Err(e) = result {
487 error!("failed to save item: {:?}, ", e);
488 }
489 })
490 .detach()
491 })
492 }
493 });
494 return;
495 }
496
497 let task = item.save(None, ctx.as_mut());
498 ctx.spawn(task, |_, result, _| {
499 if let Err(e) = result {
500 error!("failed to save item: {:?}, ", e);
501 }
502 })
503 .detach()
504 }
505 });
506 }
507
508 pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
509 match to_string_pretty(&ctx.debug_elements()) {
510 Ok(json) => {
511 let kib = json.len() as f32 / 1024.;
512 ctx.as_mut().write_to_clipboard(ClipboardItem::new(json));
513 log::info!(
514 "copied {:.1} KiB of element debug JSON to the clipboard",
515 kib
516 );
517 }
518 Err(error) => {
519 log::error!("error debugging elements: {}", error);
520 }
521 };
522 }
523
524 fn add_pane(&mut self, ctx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
525 let pane = ctx.add_view(|_| Pane::new(self.settings.clone()));
526 let pane_id = pane.id();
527 ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
528 me.handle_pane_event(pane_id, event, ctx)
529 });
530 self.panes.push(pane.clone());
531 self.activate_pane(pane.clone(), ctx);
532 pane
533 }
534
535 fn activate_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
536 self.active_pane = pane;
537 ctx.focus(&self.active_pane);
538 ctx.notify();
539 }
540
541 fn handle_pane_event(
542 &mut self,
543 pane_id: usize,
544 event: &pane::Event,
545 ctx: &mut ViewContext<Self>,
546 ) {
547 if let Some(pane) = self.pane(pane_id) {
548 match event {
549 pane::Event::Split(direction) => {
550 self.split_pane(pane, *direction, ctx);
551 }
552 pane::Event::Remove => {
553 self.remove_pane(pane, ctx);
554 }
555 pane::Event::Activate => {
556 self.activate_pane(pane, ctx);
557 }
558 }
559 } else {
560 error!("pane {} not found", pane_id);
561 }
562 }
563
564 fn split_pane(
565 &mut self,
566 pane: ViewHandle<Pane>,
567 direction: SplitDirection,
568 ctx: &mut ViewContext<Self>,
569 ) -> ViewHandle<Pane> {
570 let new_pane = self.add_pane(ctx);
571 self.activate_pane(new_pane.clone(), ctx);
572 if let Some(item) = pane.read(ctx).active_item() {
573 if let Some(clone) = item.clone_on_split(ctx.as_mut()) {
574 self.add_item(clone, ctx);
575 }
576 }
577 self.center
578 .split(pane.id(), new_pane.id(), direction)
579 .unwrap();
580 ctx.notify();
581 new_pane
582 }
583
584 fn remove_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
585 if self.center.remove(pane.id()).unwrap() {
586 self.panes.retain(|p| p != &pane);
587 self.activate_pane(self.panes.last().unwrap().clone(), ctx);
588 }
589 }
590
591 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
592 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
593 }
594
595 pub fn active_pane(&self) -> &ViewHandle<Pane> {
596 &self.active_pane
597 }
598
599 fn add_item(&self, item: Box<dyn ItemViewHandle>, ctx: &mut ViewContext<Self>) {
600 let active_pane = self.active_pane();
601 item.set_parent_pane(&active_pane, ctx.as_mut());
602 active_pane.update(ctx, |pane, ctx| {
603 let item_idx = pane.add_item(item, ctx);
604 pane.activate_item(item_idx, ctx);
605 });
606 }
607}
608
609impl Entity for Workspace {
610 type Event = ();
611}
612
613impl View for Workspace {
614 fn ui_name() -> &'static str {
615 "Workspace"
616 }
617
618 fn render(&self, _: &AppContext) -> ElementBox {
619 Container::new(
620 // self.center.render(bump)
621 Stack::new()
622 .with_child(self.center.render())
623 .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
624 .boxed(),
625 )
626 .with_background_color(rgbu(0xea, 0xea, 0xeb))
627 .named("workspace")
628 }
629
630 fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
631 ctx.focus(&self.active_pane);
632 }
633}
634
635#[cfg(test)]
636pub trait WorkspaceHandle {
637 fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc<Path>)>;
638}
639
640#[cfg(test)]
641impl WorkspaceHandle for ViewHandle<Workspace> {
642 fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc<Path>)> {
643 self.read(app)
644 .worktrees()
645 .iter()
646 .flat_map(|tree| {
647 let tree_id = tree.id();
648 tree.read(app)
649 .files(0)
650 .map(move |f| (tree_id, f.path().clone()))
651 })
652 .collect::<Vec<_>>()
653 }
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659 use crate::{editor::BufferView, settings, test::temp_tree};
660 use gpui::App;
661 use serde_json::json;
662 use std::{collections::HashSet, os::unix};
663
664 #[test]
665 fn test_open_paths_action() {
666 App::test((), |app| {
667 let settings = settings::channel(&app.font_cache()).unwrap().1;
668
669 init(app);
670
671 let dir = temp_tree(json!({
672 "a": {
673 "aa": null,
674 "ab": null,
675 },
676 "b": {
677 "ba": null,
678 "bb": null,
679 },
680 "c": {
681 "ca": null,
682 "cb": null,
683 },
684 }));
685
686 app.dispatch_global_action(
687 "workspace:open_paths",
688 OpenParams {
689 paths: vec![
690 dir.path().join("a").to_path_buf(),
691 dir.path().join("b").to_path_buf(),
692 ],
693 settings: settings.clone(),
694 },
695 );
696 assert_eq!(app.window_ids().count(), 1);
697
698 app.dispatch_global_action(
699 "workspace:open_paths",
700 OpenParams {
701 paths: vec![dir.path().join("a").to_path_buf()],
702 settings: settings.clone(),
703 },
704 );
705 assert_eq!(app.window_ids().count(), 1);
706 let workspace_view_1 = app
707 .root_view::<Workspace>(app.window_ids().next().unwrap())
708 .unwrap();
709 assert_eq!(workspace_view_1.read(app).worktrees().len(), 2);
710
711 app.dispatch_global_action(
712 "workspace:open_paths",
713 OpenParams {
714 paths: vec![
715 dir.path().join("b").to_path_buf(),
716 dir.path().join("c").to_path_buf(),
717 ],
718 settings: settings.clone(),
719 },
720 );
721 assert_eq!(app.window_ids().count(), 2);
722 });
723 }
724
725 #[test]
726 fn test_open_entry() {
727 App::test_async((), |mut app| async move {
728 let dir = temp_tree(json!({
729 "a": {
730 "file1": "contents 1",
731 "file2": "contents 2",
732 "file3": "contents 3",
733 },
734 }));
735
736 let settings = settings::channel(&app.font_cache()).unwrap().1;
737
738 let (_, workspace) = app.add_window(|ctx| {
739 let mut workspace = Workspace::new(0, settings, ctx);
740 workspace.add_worktree(dir.path(), ctx);
741 workspace
742 });
743
744 app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
745 .await;
746 let entries = app.read(|ctx| workspace.file_entries(ctx));
747 let file1 = entries[0].clone();
748 let file2 = entries[1].clone();
749 let file3 = entries[2].clone();
750
751 let pane = app.read(|ctx| workspace.read(ctx).active_pane().clone());
752
753 // Open the first entry
754 workspace
755 .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
756 .unwrap()
757 .await;
758 app.read(|ctx| {
759 let pane = pane.read(ctx);
760 assert_eq!(
761 pane.active_item().unwrap().entry_id(ctx),
762 Some(file1.clone())
763 );
764 assert_eq!(pane.items().len(), 1);
765 });
766
767 // Open the second entry
768 workspace
769 .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx))
770 .unwrap()
771 .await;
772 app.read(|ctx| {
773 let pane = pane.read(ctx);
774 assert_eq!(
775 pane.active_item().unwrap().entry_id(ctx),
776 Some(file2.clone())
777 );
778 assert_eq!(pane.items().len(), 2);
779 });
780
781 // Open the first entry again. The existing pane item is activated.
782 workspace.update(&mut app, |w, ctx| {
783 assert!(w.open_entry(file1.clone(), ctx).is_none())
784 });
785 app.read(|ctx| {
786 let pane = pane.read(ctx);
787 assert_eq!(
788 pane.active_item().unwrap().entry_id(ctx),
789 Some(file1.clone())
790 );
791 assert_eq!(pane.items().len(), 2);
792 });
793
794 // Open the third entry twice concurrently. Only one pane item is added.
795 workspace
796 .update(&mut app, |w, ctx| {
797 let task = w.open_entry(file3.clone(), ctx).unwrap();
798 assert!(w.open_entry(file3.clone(), ctx).is_none());
799 task
800 })
801 .await;
802 app.read(|ctx| {
803 let pane = pane.read(ctx);
804 assert_eq!(
805 pane.active_item().unwrap().entry_id(ctx),
806 Some(file3.clone())
807 );
808 assert_eq!(pane.items().len(), 3);
809 });
810 });
811 }
812
813 #[test]
814 fn test_open_paths() {
815 App::test_async((), |mut app| async move {
816 let dir1 = temp_tree(json!({
817 "a.txt": "",
818 }));
819 let dir2 = temp_tree(json!({
820 "b.txt": "",
821 }));
822
823 let settings = settings::channel(&app.font_cache()).unwrap().1;
824 let (_, workspace) = app.add_window(|ctx| {
825 let mut workspace = Workspace::new(0, settings, ctx);
826 workspace.add_worktree(dir1.path(), ctx);
827 workspace
828 });
829 app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
830 .await;
831
832 // Open a file within an existing worktree.
833 app.update(|ctx| {
834 workspace.update(ctx, |view, ctx| {
835 view.open_paths(&[dir1.path().join("a.txt")], ctx)
836 })
837 })
838 .await;
839 app.read(|ctx| {
840 workspace
841 .read(ctx)
842 .active_pane()
843 .read(ctx)
844 .active_item()
845 .unwrap()
846 .title(ctx)
847 == "a.txt"
848 });
849
850 // Open a file outside of any existing worktree.
851 app.update(|ctx| {
852 workspace.update(ctx, |view, ctx| {
853 view.open_paths(&[dir2.path().join("b.txt")], ctx)
854 })
855 })
856 .await;
857 app.update(|ctx| {
858 let worktree_roots = workspace
859 .read(ctx)
860 .worktrees()
861 .iter()
862 .map(|w| w.read(ctx).abs_path())
863 .collect::<HashSet<_>>();
864 assert_eq!(
865 worktree_roots,
866 vec![dir1.path(), &dir2.path().join("b.txt")]
867 .into_iter()
868 .collect(),
869 );
870 });
871 app.read(|ctx| {
872 workspace
873 .read(ctx)
874 .active_pane()
875 .read(ctx)
876 .active_item()
877 .unwrap()
878 .title(ctx)
879 == "b.txt"
880 });
881 });
882 }
883
884 #[test]
885 fn test_open_two_paths_to_the_same_file() {
886 use crate::workspace::ItemViewHandle;
887
888 App::test_async((), |mut app| async move {
889 // Create a worktree with a symlink:
890 // dir
891 // ├── hello.txt
892 // └── hola.txt -> hello.txt
893 let temp_dir = temp_tree(json!({ "hello.txt": "hi" }));
894 let dir = temp_dir.path();
895 unix::fs::symlink(dir.join("hello.txt"), dir.join("hola.txt")).unwrap();
896
897 let settings = settings::channel(&app.font_cache()).unwrap().1;
898 let (_, workspace) = app.add_window(|ctx| {
899 let mut workspace = Workspace::new(0, settings, ctx);
900 workspace.add_worktree(dir, ctx);
901 workspace
902 });
903 app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
904 .await;
905
906 // Simultaneously open both the original file and the symlink to the same file.
907 app.update(|ctx| {
908 workspace.update(ctx, |view, ctx| {
909 view.open_paths(&[dir.join("hello.txt"), dir.join("hola.txt")], ctx)
910 })
911 })
912 .await;
913
914 // The same content shows up with two different editors.
915 let buffer_views = app.read(|ctx| {
916 workspace
917 .read(ctx)
918 .active_pane()
919 .read(ctx)
920 .items()
921 .iter()
922 .map(|i| i.to_any().downcast::<BufferView>().unwrap())
923 .collect::<Vec<_>>()
924 });
925 app.read(|ctx| {
926 assert_eq!(buffer_views[0].title(ctx), "hello.txt");
927 assert_eq!(buffer_views[1].title(ctx), "hola.txt");
928 assert_eq!(buffer_views[0].read(ctx).text(ctx), "hi");
929 assert_eq!(buffer_views[1].read(ctx).text(ctx), "hi");
930 });
931
932 // When modifying one buffer, the changes appear in both editors.
933 app.update(|ctx| {
934 buffer_views[0].update(ctx, |buf, ctx| {
935 buf.insert(&"oh, ".to_string(), ctx);
936 });
937 });
938 app.read(|ctx| {
939 assert_eq!(buffer_views[0].read(ctx).text(ctx), "oh, hi");
940 assert_eq!(buffer_views[1].read(ctx).text(ctx), "oh, hi");
941 });
942 });
943 }
944
945 #[test]
946 fn test_pane_actions() {
947 App::test_async((), |mut app| async move {
948 app.update(|ctx| pane::init(ctx));
949
950 let dir = temp_tree(json!({
951 "a": {
952 "file1": "contents 1",
953 "file2": "contents 2",
954 "file3": "contents 3",
955 },
956 }));
957
958 let settings = settings::channel(&app.font_cache()).unwrap().1;
959 let (window_id, workspace) = app.add_window(|ctx| {
960 let mut workspace = Workspace::new(0, settings, ctx);
961 workspace.add_worktree(dir.path(), ctx);
962 workspace
963 });
964 app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
965 .await;
966 let entries = app.read(|ctx| workspace.file_entries(ctx));
967 let file1 = entries[0].clone();
968
969 let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone());
970
971 workspace
972 .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
973 .unwrap()
974 .await;
975 app.read(|ctx| {
976 assert_eq!(
977 pane_1.read(ctx).active_item().unwrap().entry_id(ctx),
978 Some(file1.clone())
979 );
980 });
981
982 app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
983 app.update(|ctx| {
984 let pane_2 = workspace.read(ctx).active_pane().clone();
985 assert_ne!(pane_1, pane_2);
986
987 let pane2_item = pane_2.read(ctx).active_item().unwrap();
988 assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone()));
989
990 ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
991 let workspace_view = workspace.read(ctx);
992 assert_eq!(workspace_view.panes.len(), 1);
993 assert_eq!(workspace_view.active_pane(), &pane_1);
994 });
995 });
996 }
997}