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