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