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