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