1pub mod pane;
2pub mod pane_group;
3use crate::{
4 editor::{Buffer, Editor},
5 language::LanguageRegistry,
6 settings::Settings,
7 time::ReplicaId,
8 worktree::{FileHandle, Worktree, WorktreeHandle},
9 AppState,
10};
11use futures_core::Future;
12use gpui::{
13 color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
14 ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task,
15 View, ViewContext, ViewHandle, WeakModelHandle,
16};
17use log::error;
18pub use pane::*;
19pub use pane_group::*;
20use postage::watch;
21use smol::prelude::*;
22use std::{collections::HashMap, path::PathBuf};
23use std::{
24 collections::{hash_map::Entry, HashSet},
25 path::Path,
26 sync::Arc,
27};
28
29pub fn init(cx: &mut MutableAppContext) {
30 cx.add_global_action("workspace:open", open);
31 cx.add_global_action("workspace:open_paths", open_paths);
32 cx.add_action("workspace:save", Workspace::save_active_item);
33 cx.add_action("workspace:debug_elements", Workspace::debug_elements);
34 cx.add_action("workspace:new_file", Workspace::open_new_file);
35 cx.add_bindings(vec![
36 Binding::new("cmd-s", "workspace:save", None),
37 Binding::new("cmd-alt-i", "workspace:debug_elements", None),
38 ]);
39 pane::init(cx);
40}
41
42pub struct OpenParams {
43 pub paths: Vec<PathBuf>,
44 pub app_state: AppState,
45}
46
47fn open(app_state: &AppState, cx: &mut MutableAppContext) {
48 let app_state = app_state.clone();
49 cx.prompt_for_paths(
50 PathPromptOptions {
51 files: true,
52 directories: true,
53 multiple: true,
54 },
55 move |paths, cx| {
56 if let Some(paths) = paths {
57 cx.dispatch_global_action("workspace:open_paths", OpenParams { paths, app_state });
58 }
59 },
60 );
61}
62
63fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) {
64 log::info!("open paths {:?}", params.paths);
65
66 // Open paths in existing workspace if possible
67 for window_id in cx.window_ids().collect::<Vec<_>>() {
68 if let Some(handle) = cx.root_view::<Workspace>(window_id) {
69 if handle.update(cx, |view, cx| {
70 if view.contains_paths(¶ms.paths, cx.as_ref()) {
71 let open_paths = view.open_paths(¶ms.paths, cx);
72 cx.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 cx.add_window(|cx| {
88 let mut view = Workspace::new(
89 0,
90 params.app_state.settings.clone(),
91 params.app_state.language_registry.clone(),
92 cx,
93 );
94 let open_paths = view.open_paths(¶ms.paths, cx);
95 cx.foreground().spawn(open_paths).detach();
96 view
97 });
98}
99
100pub trait Item: Entity + Sized {
101 type View: ItemView;
102
103 fn build_view(
104 handle: ModelHandle<Self>,
105 settings: watch::Receiver<Settings>,
106 cx: &mut ViewContext<Self::View>,
107 ) -> Self::View;
108
109 fn file(&self) -> Option<&FileHandle>;
110}
111
112pub trait ItemView: View {
113 fn title(&self, cx: &AppContext) -> String;
114 fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)>;
115 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
116 where
117 Self: Sized,
118 {
119 None
120 }
121 fn is_dirty(&self, _: &AppContext) -> bool {
122 false
123 }
124 fn has_conflict(&self, _: &AppContext) -> bool {
125 false
126 }
127 fn save(
128 &mut self,
129 _: Option<FileHandle>,
130 _: &mut ViewContext<Self>,
131 ) -> Task<anyhow::Result<()>>;
132 fn should_activate_item_on_event(_: &Self::Event) -> bool {
133 false
134 }
135 fn should_update_tab_on_event(_: &Self::Event) -> bool {
136 false
137 }
138}
139
140pub trait ItemHandle: Send + Sync {
141 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
142 fn downgrade(&self) -> Box<dyn WeakItemHandle>;
143}
144
145pub trait WeakItemHandle: Send + Sync {
146 fn file<'a>(&'a self, cx: &'a AppContext) -> Option<&'a FileHandle>;
147 fn add_view(
148 &self,
149 window_id: usize,
150 settings: watch::Receiver<Settings>,
151 cx: &mut MutableAppContext,
152 ) -> Option<Box<dyn ItemViewHandle>>;
153 fn alive(&self, cx: &AppContext) -> bool;
154}
155
156pub trait ItemViewHandle: Send + Sync {
157 fn title(&self, cx: &AppContext) -> String;
158 fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)>;
159 fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
160 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
161 fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext);
162 fn id(&self) -> usize;
163 fn to_any(&self) -> AnyViewHandle;
164 fn is_dirty(&self, cx: &AppContext) -> bool;
165 fn has_conflict(&self, cx: &AppContext) -> bool;
166 fn save(
167 &self,
168 file: Option<FileHandle>,
169 cx: &mut MutableAppContext,
170 ) -> Task<anyhow::Result<()>>;
171}
172
173impl<T: Item> ItemHandle for ModelHandle<T> {
174 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
175 Box::new(self.clone())
176 }
177
178 fn downgrade(&self) -> Box<dyn WeakItemHandle> {
179 Box::new(self.downgrade())
180 }
181}
182
183impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
184 fn file<'a>(&'a self, cx: &'a AppContext) -> Option<&'a FileHandle> {
185 self.upgrade(cx).and_then(|h| h.read(cx).file())
186 }
187
188 fn add_view(
189 &self,
190 window_id: usize,
191 settings: watch::Receiver<Settings>,
192 cx: &mut MutableAppContext,
193 ) -> Option<Box<dyn ItemViewHandle>> {
194 if let Some(handle) = self.upgrade(cx.as_ref()) {
195 Some(Box::new(cx.add_view(window_id, |cx| {
196 T::build_view(handle, settings, cx)
197 })))
198 } else {
199 None
200 }
201 }
202
203 fn alive(&self, cx: &AppContext) -> bool {
204 self.upgrade(cx).is_some()
205 }
206}
207
208impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
209 fn title(&self, cx: &AppContext) -> String {
210 self.read(cx).title(cx)
211 }
212
213 fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc<Path>)> {
214 self.read(cx).entry_id(cx)
215 }
216
217 fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
218 Box::new(self.clone())
219 }
220
221 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
222 self.update(cx, |item, cx| {
223 cx.add_option_view(|cx| item.clone_on_split(cx))
224 })
225 .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
226 }
227
228 fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext) {
229 pane.update(cx, |_, cx| {
230 cx.subscribe_to_view(self, |pane, item, event, cx| {
231 if T::should_activate_item_on_event(event) {
232 if let Some(ix) = pane.item_index(&item) {
233 pane.activate_item(ix, cx);
234 pane.activate(cx);
235 }
236 }
237 if T::should_update_tab_on_event(event) {
238 cx.notify()
239 }
240 })
241 })
242 }
243
244 fn save(
245 &self,
246 file: Option<FileHandle>,
247 cx: &mut MutableAppContext,
248 ) -> Task<anyhow::Result<()>> {
249 self.update(cx, |item, cx| item.save(file, cx))
250 }
251
252 fn is_dirty(&self, cx: &AppContext) -> bool {
253 self.read(cx).is_dirty(cx)
254 }
255
256 fn has_conflict(&self, cx: &AppContext) -> bool {
257 self.read(cx).has_conflict(cx)
258 }
259
260 fn id(&self) -> usize {
261 self.id()
262 }
263
264 fn to_any(&self) -> AnyViewHandle {
265 self.into()
266 }
267}
268
269impl Clone for Box<dyn ItemViewHandle> {
270 fn clone(&self) -> Box<dyn ItemViewHandle> {
271 self.boxed_clone()
272 }
273}
274
275impl Clone for Box<dyn ItemHandle> {
276 fn clone(&self) -> Box<dyn ItemHandle> {
277 self.boxed_clone()
278 }
279}
280
281#[derive(Debug)]
282pub struct State {
283 pub modal: Option<usize>,
284 pub center: PaneGroup,
285}
286
287pub struct Workspace {
288 pub settings: watch::Receiver<Settings>,
289 language_registry: Arc<LanguageRegistry>,
290 modal: Option<AnyViewHandle>,
291 center: PaneGroup,
292 panes: Vec<ViewHandle<Pane>>,
293 active_pane: ViewHandle<Pane>,
294 replica_id: ReplicaId,
295 worktrees: HashSet<ModelHandle<Worktree>>,
296 items: Vec<Box<dyn WeakItemHandle>>,
297 loading_items: HashMap<
298 (usize, Arc<Path>),
299 postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
300 >,
301}
302
303impl Workspace {
304 pub fn new(
305 replica_id: ReplicaId,
306 settings: watch::Receiver<Settings>,
307 language_registry: Arc<LanguageRegistry>,
308 cx: &mut ViewContext<Self>,
309 ) -> Self {
310 let pane = cx.add_view(|_| Pane::new(settings.clone()));
311 let pane_id = pane.id();
312 cx.subscribe_to_view(&pane, move |me, _, event, cx| {
313 me.handle_pane_event(pane_id, event, cx)
314 });
315 cx.focus(&pane);
316
317 Workspace {
318 modal: None,
319 center: PaneGroup::new(pane.id()),
320 panes: vec![pane.clone()],
321 active_pane: pane.clone(),
322 settings,
323 language_registry,
324 replica_id,
325 worktrees: Default::default(),
326 items: Default::default(),
327 loading_items: Default::default(),
328 }
329 }
330
331 pub fn worktrees(&self) -> &HashSet<ModelHandle<Worktree>> {
332 &self.worktrees
333 }
334
335 pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
336 paths.iter().all(|path| self.contains_path(&path, cx))
337 }
338
339 pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
340 self.worktrees
341 .iter()
342 .any(|worktree| worktree.read(cx).contains_abs_path(path))
343 }
344
345 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
346 let futures = self
347 .worktrees
348 .iter()
349 .map(|worktree| worktree.read(cx).scan_complete())
350 .collect::<Vec<_>>();
351 async move {
352 for future in futures {
353 future.await;
354 }
355 }
356 }
357
358 pub fn open_paths(
359 &mut self,
360 abs_paths: &[PathBuf],
361 cx: &mut ViewContext<Self>,
362 ) -> impl Future<Output = ()> {
363 let entries = abs_paths
364 .iter()
365 .cloned()
366 .map(|path| self.file_for_path(&path, cx))
367 .collect::<Vec<_>>();
368
369 let bg = cx.background_executor().clone();
370 let tasks = abs_paths
371 .iter()
372 .cloned()
373 .zip(entries.into_iter())
374 .map(|(abs_path, file)| {
375 let is_file = bg.spawn(async move { abs_path.is_file() });
376 cx.spawn(|this, mut cx| async move {
377 let file = file.await;
378 let is_file = is_file.await;
379 this.update(&mut cx, |this, cx| {
380 if is_file {
381 this.open_entry(file.entry_id(), cx)
382 } else {
383 None
384 }
385 })
386 })
387 })
388 .collect::<Vec<_>>();
389 async move {
390 for task in tasks {
391 if let Some(task) = task.await {
392 task.await;
393 }
394 }
395 }
396 }
397
398 fn file_for_path(&mut self, abs_path: &Path, cx: &mut ViewContext<Self>) -> Task<FileHandle> {
399 for tree in self.worktrees.iter() {
400 if let Ok(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()) {
401 return tree.file(relative_path, cx.as_mut());
402 }
403 }
404 let worktree = self.add_worktree(&abs_path, cx);
405 worktree.file(Path::new(""), cx.as_mut())
406 }
407
408 pub fn add_worktree(
409 &mut self,
410 path: &Path,
411 cx: &mut ViewContext<Self>,
412 ) -> ModelHandle<Worktree> {
413 let worktree = cx.add_model(|cx| Worktree::new(path, cx));
414 cx.observe_model(&worktree, |_, _, cx| cx.notify());
415 self.worktrees.insert(worktree.clone());
416 cx.notify();
417 worktree
418 }
419
420 pub fn toggle_modal<V, F>(&mut self, cx: &mut ViewContext<Self>, add_view: F)
421 where
422 V: 'static + View,
423 F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
424 {
425 if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
426 self.modal.take();
427 cx.focus_self();
428 } else {
429 let modal = add_view(cx, self);
430 cx.focus(&modal);
431 self.modal = Some(modal.into());
432 }
433 cx.notify();
434 }
435
436 pub fn modal(&self) -> Option<&AnyViewHandle> {
437 self.modal.as_ref()
438 }
439
440 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
441 if self.modal.take().is_some() {
442 cx.focus(&self.active_pane);
443 cx.notify();
444 }
445 }
446
447 pub fn open_new_file(&mut self, _: &(), cx: &mut ViewContext<Self>) {
448 let buffer = cx.add_model(|cx| Buffer::new(self.replica_id, "", cx));
449 let buffer_view =
450 cx.add_view(|cx| Editor::for_buffer(buffer.clone(), self.settings.clone(), cx));
451 self.items.push(ItemHandle::downgrade(&buffer));
452 self.add_item_view(Box::new(buffer_view), cx);
453 }
454
455 #[must_use]
456 pub fn open_entry(
457 &mut self,
458 entry: (usize, Arc<Path>),
459 cx: &mut ViewContext<Self>,
460 ) -> Option<Task<()>> {
461 // If the active pane contains a view for this file, then activate
462 // that item view.
463 if self
464 .active_pane()
465 .update(cx, |pane, cx| pane.activate_entry(entry.clone(), cx))
466 {
467 return None;
468 }
469
470 // Otherwise, if this file is already open somewhere in the workspace,
471 // then add another view for it.
472 let settings = self.settings.clone();
473 let mut view_for_existing_item = None;
474 self.items.retain(|item| {
475 if item.alive(cx.as_ref()) {
476 if view_for_existing_item.is_none()
477 && item
478 .file(cx.as_ref())
479 .map_or(false, |f| f.entry_id() == entry)
480 {
481 view_for_existing_item = Some(
482 item.add_view(cx.window_id(), settings.clone(), cx.as_mut())
483 .unwrap(),
484 );
485 }
486 true
487 } else {
488 false
489 }
490 });
491 if let Some(view) = view_for_existing_item {
492 self.add_item_view(view, cx);
493 return None;
494 }
495
496 let (worktree_id, path) = entry.clone();
497
498 let worktree = match self.worktrees.get(&worktree_id).cloned() {
499 Some(worktree) => worktree,
500 None => {
501 log::error!("worktree {} does not exist", worktree_id);
502 return None;
503 }
504 };
505
506 let file = worktree.file(path.clone(), cx.as_mut());
507 if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) {
508 let (mut tx, rx) = postage::watch::channel();
509 entry.insert(rx);
510 let replica_id = self.replica_id;
511 let language_registry = self.language_registry.clone();
512
513 cx.as_mut()
514 .spawn(|mut cx| async move {
515 let file = file.await;
516 let history = cx.read(|cx| file.load_history(cx));
517 let history = cx.background_executor().spawn(history).await;
518
519 *tx.borrow_mut() = Some(match history {
520 Ok(history) => Ok(Box::new(cx.add_model(|cx| {
521 let language = language_registry.select_language(path);
522 Buffer::from_history(
523 replica_id,
524 history,
525 Some(file),
526 language.cloned(),
527 cx,
528 )
529 }))),
530 Err(error) => Err(Arc::new(error)),
531 })
532 })
533 .detach();
534 }
535
536 let mut watch = self.loading_items.get(&entry).unwrap().clone();
537
538 Some(cx.spawn(|this, mut cx| async move {
539 let load_result = loop {
540 if let Some(load_result) = watch.borrow().as_ref() {
541 break load_result.clone();
542 }
543 watch.next().await;
544 };
545
546 this.update(&mut cx, |this, cx| {
547 this.loading_items.remove(&entry);
548 match load_result {
549 Ok(item) => {
550 let weak_item = item.downgrade();
551 let view = weak_item
552 .add_view(cx.window_id(), settings, cx.as_mut())
553 .unwrap();
554 this.items.push(weak_item);
555 this.add_item_view(view, cx);
556 }
557 Err(error) => {
558 log::error!("error opening item: {}", error);
559 }
560 }
561 })
562 }))
563 }
564
565 pub fn active_item(&self, cx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
566 self.active_pane().read(cx).active_item()
567 }
568
569 pub fn save_active_item(&mut self, _: &(), cx: &mut ViewContext<Self>) {
570 if let Some(item) = self.active_item(cx) {
571 let handle = cx.handle();
572 if item.entry_id(cx.as_ref()).is_none() {
573 let start_path = self
574 .worktrees
575 .iter()
576 .next()
577 .map_or(Path::new(""), |h| h.read(cx).abs_path())
578 .to_path_buf();
579 cx.prompt_for_new_path(&start_path, move |path, cx| {
580 if let Some(path) = path {
581 cx.spawn(|mut cx| async move {
582 let file = handle
583 .update(&mut cx, |me, cx| me.file_for_path(&path, cx))
584 .await;
585 if let Err(error) = cx.update(|cx| item.save(Some(file), cx)).await {
586 error!("failed to save item: {:?}, ", error);
587 }
588 })
589 .detach()
590 }
591 });
592 return;
593 } else if item.has_conflict(cx.as_ref()) {
594 const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
595
596 cx.prompt(
597 PromptLevel::Warning,
598 CONFLICT_MESSAGE,
599 &["Overwrite", "Cancel"],
600 move |answer, cx| {
601 if answer == 0 {
602 cx.spawn(|mut cx| async move {
603 if let Err(error) = cx.update(|cx| item.save(None, cx)).await {
604 error!("failed to save item: {:?}, ", error);
605 }
606 })
607 .detach();
608 }
609 },
610 );
611 } else {
612 cx.spawn(|_, mut cx| async move {
613 if let Err(error) = cx.update(|cx| item.save(None, cx)).await {
614 error!("failed to save item: {:?}, ", error);
615 }
616 })
617 .detach();
618 }
619 }
620 }
621
622 pub fn debug_elements(&mut self, _: &(), cx: &mut ViewContext<Self>) {
623 match to_string_pretty(&cx.debug_elements()) {
624 Ok(json) => {
625 let kib = json.len() as f32 / 1024.;
626 cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
627 log::info!(
628 "copied {:.1} KiB of element debug JSON to the clipboard",
629 kib
630 );
631 }
632 Err(error) => {
633 log::error!("error debugging elements: {}", error);
634 }
635 };
636 }
637
638 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
639 let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
640 let pane_id = pane.id();
641 cx.subscribe_to_view(&pane, move |me, _, event, cx| {
642 me.handle_pane_event(pane_id, event, cx)
643 });
644 self.panes.push(pane.clone());
645 self.activate_pane(pane.clone(), cx);
646 pane
647 }
648
649 fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
650 self.active_pane = pane;
651 cx.focus(&self.active_pane);
652 cx.notify();
653 }
654
655 fn handle_pane_event(
656 &mut self,
657 pane_id: usize,
658 event: &pane::Event,
659 cx: &mut ViewContext<Self>,
660 ) {
661 if let Some(pane) = self.pane(pane_id) {
662 match event {
663 pane::Event::Split(direction) => {
664 self.split_pane(pane, *direction, cx);
665 }
666 pane::Event::Remove => {
667 self.remove_pane(pane, cx);
668 }
669 pane::Event::Activate => {
670 self.activate_pane(pane, cx);
671 }
672 }
673 } else {
674 error!("pane {} not found", pane_id);
675 }
676 }
677
678 fn split_pane(
679 &mut self,
680 pane: ViewHandle<Pane>,
681 direction: SplitDirection,
682 cx: &mut ViewContext<Self>,
683 ) -> ViewHandle<Pane> {
684 let new_pane = self.add_pane(cx);
685 self.activate_pane(new_pane.clone(), cx);
686 if let Some(item) = pane.read(cx).active_item() {
687 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
688 self.add_item_view(clone, cx);
689 }
690 }
691 self.center
692 .split(pane.id(), new_pane.id(), direction)
693 .unwrap();
694 cx.notify();
695 new_pane
696 }
697
698 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
699 if self.center.remove(pane.id()).unwrap() {
700 self.panes.retain(|p| p != &pane);
701 self.activate_pane(self.panes.last().unwrap().clone(), cx);
702 }
703 }
704
705 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
706 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
707 }
708
709 pub fn active_pane(&self) -> &ViewHandle<Pane> {
710 &self.active_pane
711 }
712
713 fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut ViewContext<Self>) {
714 let active_pane = self.active_pane();
715 item.set_parent_pane(&active_pane, cx.as_mut());
716 active_pane.update(cx, |pane, cx| {
717 let item_idx = pane.add_item(item, cx);
718 pane.activate_item(item_idx, cx);
719 });
720 }
721}
722
723impl Entity for Workspace {
724 type Event = ();
725}
726
727impl View for Workspace {
728 fn ui_name() -> &'static str {
729 "Workspace"
730 }
731
732 fn render(&self, _: &AppContext) -> ElementBox {
733 Container::new(
734 // self.center.render(bump)
735 Stack::new()
736 .with_child(self.center.render())
737 .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
738 .boxed(),
739 )
740 .with_background_color(rgbu(0xea, 0xea, 0xeb))
741 .named("workspace")
742 }
743
744 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
745 cx.focus(&self.active_pane);
746 }
747}
748
749#[cfg(test)]
750pub trait WorkspaceHandle {
751 fn file_entries(&self, cx: &AppContext) -> Vec<(usize, Arc<Path>)>;
752}
753
754#[cfg(test)]
755impl WorkspaceHandle for ViewHandle<Workspace> {
756 fn file_entries(&self, cx: &AppContext) -> Vec<(usize, Arc<Path>)> {
757 self.read(cx)
758 .worktrees()
759 .iter()
760 .flat_map(|tree| {
761 let tree_id = tree.id();
762 tree.read(cx)
763 .files(0)
764 .map(move |f| (tree_id, f.path().clone()))
765 })
766 .collect::<Vec<_>>()
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773 use crate::{
774 editor::Editor,
775 test::{build_app_state, temp_tree},
776 };
777 use serde_json::json;
778 use std::{collections::HashSet, fs};
779 use tempdir::TempDir;
780
781 #[gpui::test]
782 fn test_open_paths_action(cx: &mut gpui::MutableAppContext) {
783 let app_state = build_app_state(cx.as_ref());
784
785 init(cx);
786
787 let dir = temp_tree(json!({
788 "a": {
789 "aa": null,
790 "ab": null,
791 },
792 "b": {
793 "ba": null,
794 "bb": null,
795 },
796 "c": {
797 "ca": null,
798 "cb": null,
799 },
800 }));
801
802 cx.dispatch_global_action(
803 "workspace:open_paths",
804 OpenParams {
805 paths: vec![
806 dir.path().join("a").to_path_buf(),
807 dir.path().join("b").to_path_buf(),
808 ],
809 app_state: app_state.clone(),
810 },
811 );
812 assert_eq!(cx.window_ids().count(), 1);
813
814 cx.dispatch_global_action(
815 "workspace:open_paths",
816 OpenParams {
817 paths: vec![dir.path().join("a").to_path_buf()],
818 app_state: app_state.clone(),
819 },
820 );
821 assert_eq!(cx.window_ids().count(), 1);
822 let workspace_view_1 = cx
823 .root_view::<Workspace>(cx.window_ids().next().unwrap())
824 .unwrap();
825 assert_eq!(workspace_view_1.read(cx).worktrees().len(), 2);
826
827 cx.dispatch_global_action(
828 "workspace:open_paths",
829 OpenParams {
830 paths: vec![
831 dir.path().join("b").to_path_buf(),
832 dir.path().join("c").to_path_buf(),
833 ],
834 app_state: app_state.clone(),
835 },
836 );
837 assert_eq!(cx.window_ids().count(), 2);
838 }
839
840 #[gpui::test]
841 async fn test_open_entry(mut cx: gpui::TestAppContext) {
842 let dir = temp_tree(json!({
843 "a": {
844 "file1": "contents 1",
845 "file2": "contents 2",
846 "file3": "contents 3",
847 },
848 }));
849
850 let app_state = cx.read(build_app_state);
851
852 let (_, workspace) = cx.add_window(|cx| {
853 let mut workspace =
854 Workspace::new(0, app_state.settings, app_state.language_registry, cx);
855 workspace.add_worktree(dir.path(), cx);
856 workspace
857 });
858
859 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
860 .await;
861 let entries = cx.read(|cx| workspace.file_entries(cx));
862 let file1 = entries[0].clone();
863 let file2 = entries[1].clone();
864 let file3 = entries[2].clone();
865
866 // Open the first entry
867 workspace
868 .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
869 .unwrap()
870 .await;
871 cx.read(|cx| {
872 let pane = workspace.read(cx).active_pane().read(cx);
873 assert_eq!(
874 pane.active_item().unwrap().entry_id(cx),
875 Some(file1.clone())
876 );
877 assert_eq!(pane.items().len(), 1);
878 });
879
880 // Open the second entry
881 workspace
882 .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx))
883 .unwrap()
884 .await;
885 cx.read(|cx| {
886 let pane = workspace.read(cx).active_pane().read(cx);
887 assert_eq!(
888 pane.active_item().unwrap().entry_id(cx),
889 Some(file2.clone())
890 );
891 assert_eq!(pane.items().len(), 2);
892 });
893
894 // Open the first entry again. The existing pane item is activated.
895 workspace.update(&mut cx, |w, cx| {
896 assert!(w.open_entry(file1.clone(), cx).is_none())
897 });
898 cx.read(|cx| {
899 let pane = workspace.read(cx).active_pane().read(cx);
900 assert_eq!(
901 pane.active_item().unwrap().entry_id(cx),
902 Some(file1.clone())
903 );
904 assert_eq!(pane.items().len(), 2);
905 });
906
907 // Split the pane with the first entry, then open the second entry again.
908 workspace.update(&mut cx, |w, cx| {
909 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
910 assert!(w.open_entry(file2.clone(), cx).is_none());
911 assert_eq!(
912 w.active_pane()
913 .read(cx)
914 .active_item()
915 .unwrap()
916 .entry_id(cx.as_ref()),
917 Some(file2.clone())
918 );
919 });
920
921 // Open the third entry twice concurrently. Two pane items
922 // are added.
923 let (t1, t2) = workspace.update(&mut cx, |w, cx| {
924 (
925 w.open_entry(file3.clone(), cx).unwrap(),
926 w.open_entry(file3.clone(), cx).unwrap(),
927 )
928 });
929 t1.await;
930 t2.await;
931 cx.read(|cx| {
932 let pane = workspace.read(cx).active_pane().read(cx);
933 assert_eq!(
934 pane.active_item().unwrap().entry_id(cx),
935 Some(file3.clone())
936 );
937 let pane_entries = pane
938 .items()
939 .iter()
940 .map(|i| i.entry_id(cx).unwrap())
941 .collect::<Vec<_>>();
942 assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
943 });
944 }
945
946 #[gpui::test]
947 async fn test_open_paths(mut cx: gpui::TestAppContext) {
948 let dir1 = temp_tree(json!({
949 "a.txt": "",
950 }));
951 let dir2 = temp_tree(json!({
952 "b.txt": "",
953 }));
954
955 let app_state = cx.read(build_app_state);
956 let (_, workspace) = cx.add_window(|cx| {
957 let mut workspace =
958 Workspace::new(0, app_state.settings, app_state.language_registry, cx);
959 workspace.add_worktree(dir1.path(), cx);
960 workspace
961 });
962 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
963 .await;
964
965 // Open a file within an existing worktree.
966 cx.update(|cx| {
967 workspace.update(cx, |view, cx| {
968 view.open_paths(&[dir1.path().join("a.txt")], cx)
969 })
970 })
971 .await;
972 cx.read(|cx| {
973 assert_eq!(
974 workspace
975 .read(cx)
976 .active_pane()
977 .read(cx)
978 .active_item()
979 .unwrap()
980 .title(cx),
981 "a.txt"
982 );
983 });
984
985 // Open a file outside of any existing worktree.
986 cx.update(|cx| {
987 workspace.update(cx, |view, cx| {
988 view.open_paths(&[dir2.path().join("b.txt")], cx)
989 })
990 })
991 .await;
992 cx.read(|cx| {
993 let worktree_roots = workspace
994 .read(cx)
995 .worktrees()
996 .iter()
997 .map(|w| w.read(cx).abs_path())
998 .collect::<HashSet<_>>();
999 assert_eq!(
1000 worktree_roots,
1001 vec![dir1.path(), &dir2.path().join("b.txt")]
1002 .into_iter()
1003 .collect(),
1004 );
1005 assert_eq!(
1006 workspace
1007 .read(cx)
1008 .active_pane()
1009 .read(cx)
1010 .active_item()
1011 .unwrap()
1012 .title(cx),
1013 "b.txt"
1014 );
1015 });
1016 }
1017
1018 #[gpui::test]
1019 async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
1020 let dir = temp_tree(json!({
1021 "a.txt": "",
1022 }));
1023
1024 let app_state = cx.read(build_app_state);
1025 let (window_id, workspace) = cx.add_window(|cx| {
1026 let mut workspace =
1027 Workspace::new(0, app_state.settings, app_state.language_registry, cx);
1028 workspace.add_worktree(dir.path(), cx);
1029 workspace
1030 });
1031 let tree = cx.read(|cx| {
1032 let mut trees = workspace.read(cx).worktrees().iter();
1033 trees.next().unwrap().clone()
1034 });
1035 tree.flush_fs_events(&cx).await;
1036
1037 // Open a file within an existing worktree.
1038 cx.update(|cx| {
1039 workspace.update(cx, |view, cx| {
1040 view.open_paths(&[dir.path().join("a.txt")], cx)
1041 })
1042 })
1043 .await;
1044 let editor = cx.read(|cx| {
1045 let pane = workspace.read(cx).active_pane().read(cx);
1046 let item = pane.active_item().unwrap();
1047 item.to_any().downcast::<Editor>().unwrap()
1048 });
1049
1050 cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&"x".to_string(), cx)));
1051 fs::write(dir.path().join("a.txt"), "changed").unwrap();
1052 editor
1053 .condition(&cx, |editor, cx| editor.has_conflict(cx))
1054 .await;
1055 cx.read(|cx| assert!(editor.is_dirty(cx)));
1056
1057 cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&(), cx)));
1058 cx.simulate_prompt_answer(window_id, 0);
1059 editor
1060 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1061 .await;
1062 cx.read(|cx| assert!(!editor.has_conflict(cx)));
1063 }
1064
1065 #[gpui::test]
1066 async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
1067 let dir = TempDir::new("test-new-file").unwrap();
1068 let app_state = cx.read(build_app_state);
1069 let (_, workspace) = cx.add_window(|cx| {
1070 let mut workspace =
1071 Workspace::new(0, app_state.settings, app_state.language_registry, cx);
1072 workspace.add_worktree(dir.path(), cx);
1073 workspace
1074 });
1075 let tree = cx.read(|cx| {
1076 workspace
1077 .read(cx)
1078 .worktrees()
1079 .iter()
1080 .next()
1081 .unwrap()
1082 .clone()
1083 });
1084 tree.flush_fs_events(&cx).await;
1085
1086 // Create a new untitled buffer
1087 let editor = workspace.update(&mut cx, |workspace, cx| {
1088 workspace.open_new_file(&(), cx);
1089 workspace
1090 .active_item(cx)
1091 .unwrap()
1092 .to_any()
1093 .downcast::<Editor>()
1094 .unwrap()
1095 });
1096 editor.update(&mut cx, |editor, cx| {
1097 assert!(!editor.is_dirty(cx.as_ref()));
1098 assert_eq!(editor.title(cx.as_ref()), "untitled");
1099 editor.insert(&"hi".to_string(), cx);
1100 assert!(editor.is_dirty(cx.as_ref()));
1101 });
1102
1103 // Save the buffer. This prompts for a filename.
1104 workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
1105 cx.simulate_new_path_selection(|parent_dir| {
1106 assert_eq!(parent_dir, dir.path());
1107 Some(parent_dir.join("the-new-name"))
1108 });
1109 cx.read(|cx| {
1110 assert!(editor.is_dirty(cx));
1111 assert_eq!(editor.title(cx), "untitled");
1112 });
1113
1114 // When the save completes, the buffer's title is updated.
1115 editor
1116 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1117 .await;
1118 cx.read(|cx| {
1119 assert!(!editor.is_dirty(cx));
1120 assert_eq!(editor.title(cx), "the-new-name");
1121 });
1122
1123 // Edit the file and save it again. This time, there is no filename prompt.
1124 editor.update(&mut cx, |editor, cx| {
1125 editor.insert(&" there".to_string(), cx);
1126 assert_eq!(editor.is_dirty(cx.as_ref()), true);
1127 });
1128 workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
1129 assert!(!cx.did_prompt_for_new_path());
1130 editor
1131 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1132 .await;
1133 cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name"));
1134
1135 // Open the same newly-created file in another pane item. The new editor should reuse
1136 // the same buffer.
1137 workspace.update(&mut cx, |workspace, cx| {
1138 workspace.open_new_file(&(), cx);
1139 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
1140 assert!(workspace
1141 .open_entry((tree.id(), Path::new("the-new-name").into()), cx)
1142 .is_none());
1143 });
1144 let editor2 = workspace.update(&mut cx, |workspace, cx| {
1145 workspace
1146 .active_item(cx)
1147 .unwrap()
1148 .to_any()
1149 .downcast::<Editor>()
1150 .unwrap()
1151 });
1152 cx.read(|cx| {
1153 assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer());
1154 })
1155 }
1156
1157 #[gpui::test]
1158 async fn test_pane_actions(mut cx: gpui::TestAppContext) {
1159 cx.update(|cx| pane::init(cx));
1160
1161 let dir = temp_tree(json!({
1162 "a": {
1163 "file1": "contents 1",
1164 "file2": "contents 2",
1165 "file3": "contents 3",
1166 },
1167 }));
1168
1169 let app_state = cx.read(build_app_state);
1170 let (window_id, workspace) = cx.add_window(|cx| {
1171 let mut workspace =
1172 Workspace::new(0, app_state.settings, app_state.language_registry, cx);
1173 workspace.add_worktree(dir.path(), cx);
1174 workspace
1175 });
1176 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1177 .await;
1178 let entries = cx.read(|cx| workspace.file_entries(cx));
1179 let file1 = entries[0].clone();
1180
1181 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1182
1183 workspace
1184 .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
1185 .unwrap()
1186 .await;
1187 cx.read(|cx| {
1188 assert_eq!(
1189 pane_1.read(cx).active_item().unwrap().entry_id(cx),
1190 Some(file1.clone())
1191 );
1192 });
1193
1194 cx.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
1195 cx.update(|cx| {
1196 let pane_2 = workspace.read(cx).active_pane().clone();
1197 assert_ne!(pane_1, pane_2);
1198
1199 let pane2_item = pane_2.read(cx).active_item().unwrap();
1200 assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone()));
1201
1202 cx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
1203 let workspace_view = workspace.read(cx);
1204 assert_eq!(workspace_view.panes.len(), 1);
1205 assert_eq!(workspace_view.active_pane(), &pane_1);
1206 });
1207 }
1208}