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