1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11pub mod tasks;
12mod theme_preview;
13mod toolbar;
14mod workspace_settings;
15
16use anyhow::{anyhow, Context as _, Result};
17use call::{call_settings::CallSettings, ActiveCall};
18use client::{
19 proto::{self, ErrorCode, PanelId, PeerId},
20 ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
21};
22use collections::{hash_map, HashMap, HashSet};
23use derive_more::{Deref, DerefMut};
24use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
25use futures::{
26 channel::{
27 mpsc::{self, UnboundedReceiver, UnboundedSender},
28 oneshot,
29 },
30 future::try_join_all,
31 Future, FutureExt, StreamExt,
32};
33use gpui::{
34 action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
35 transparent_black, Action, AnyView, AnyWeakView, AppContext, AsyncAppContext,
36 AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
37 EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
38 Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
39 Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
40 WindowId, WindowOptions,
41};
42pub use item::{
43 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
44 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
45};
46use itertools::Itertools;
47use language::{LanguageRegistry, Rope};
48pub use modal_layer::*;
49use node_runtime::NodeRuntime;
50use notifications::{simple_message_notification::MessageNotification, DetachAndPromptErr};
51pub use pane::*;
52pub use pane_group::*;
53pub use persistence::{
54 model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
55 WorkspaceDb, DB as WORKSPACE_DB,
56};
57use persistence::{
58 model::{SerializedSshProject, SerializedWorkspace},
59 SerializedWindowBounds, DB,
60};
61use postage::stream::Stream;
62use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree};
63use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
64use schemars::JsonSchema;
65use serde::Deserialize;
66use session::AppSession;
67use settings::Settings;
68use shared_screen::SharedScreen;
69use sqlez::{
70 bindable::{Bind, Column, StaticColumnCount},
71 statement::Statement,
72};
73use status_bar::StatusBar;
74pub use status_bar::StatusItemView;
75use std::{
76 any::TypeId,
77 borrow::Cow,
78 cell::RefCell,
79 cmp,
80 collections::hash_map::DefaultHasher,
81 env,
82 hash::{Hash, Hasher},
83 path::{Path, PathBuf},
84 rc::Rc,
85 sync::{atomic::AtomicUsize, Arc, LazyLock, Weak},
86 time::Duration,
87};
88use task::SpawnInTerminal;
89use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
90pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
91pub use ui;
92use ui::prelude::*;
93use util::{paths::SanitizedPath, serde::default_true, ResultExt, TryFutureExt};
94use uuid::Uuid;
95pub use workspace_settings::{
96 AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
97};
98
99use crate::notifications::NotificationId;
100use crate::persistence::{
101 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
102 SerializedAxis,
103};
104
105static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
106 env::var("ZED_WINDOW_SIZE")
107 .ok()
108 .as_deref()
109 .and_then(parse_pixel_size_env_var)
110});
111
112static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
113 env::var("ZED_WINDOW_POSITION")
114 .ok()
115 .as_deref()
116 .and_then(parse_pixel_position_env_var)
117});
118
119actions!(assistant, [ShowConfiguration]);
120
121actions!(
122 workspace,
123 [
124 ActivateNextPane,
125 ActivatePreviousPane,
126 ActivateNextWindow,
127 ActivatePreviousWindow,
128 AddFolderToProject,
129 ClearAllNotifications,
130 CloseAllDocks,
131 CloseWindow,
132 CopyPath,
133 CopyRelativePath,
134 Feedback,
135 FollowNextCollaborator,
136 MoveFocusedPanelToNextPosition,
137 NewCenterTerminal,
138 NewFile,
139 NewFileSplitVertical,
140 NewFileSplitHorizontal,
141 NewSearch,
142 NewTerminal,
143 NewWindow,
144 Open,
145 OpenFiles,
146 OpenInTerminal,
147 ReloadActiveItem,
148 SaveAs,
149 SaveWithoutFormat,
150 ToggleBottomDock,
151 ToggleCenteredLayout,
152 ToggleLeftDock,
153 ToggleRightDock,
154 ToggleZoom,
155 Unfollow,
156 Welcome,
157 ]
158);
159
160#[derive(Clone, PartialEq)]
161pub struct OpenPaths {
162 pub paths: Vec<PathBuf>,
163}
164
165#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
166pub struct ActivatePane(pub usize);
167
168#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
169pub struct ActivatePaneInDirection(pub SplitDirection);
170
171#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
172pub struct SwapPaneInDirection(pub SplitDirection);
173
174#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
175pub struct MoveItemToPane {
176 pub destination: usize,
177 #[serde(default = "default_true")]
178 pub focus: bool,
179}
180
181#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
182pub struct MoveItemToPaneInDirection {
183 pub direction: SplitDirection,
184 #[serde(default = "default_true")]
185 pub focus: bool,
186}
187
188#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
189#[serde(rename_all = "camelCase")]
190pub struct SaveAll {
191 pub save_intent: Option<SaveIntent>,
192}
193
194#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
195#[serde(rename_all = "camelCase")]
196pub struct Save {
197 pub save_intent: Option<SaveIntent>,
198}
199
200#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
201#[serde(rename_all = "camelCase")]
202pub struct CloseAllItemsAndPanes {
203 pub save_intent: Option<SaveIntent>,
204}
205
206#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
207#[serde(rename_all = "camelCase")]
208pub struct CloseInactiveTabsAndPanes {
209 pub save_intent: Option<SaveIntent>,
210}
211
212#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
213pub struct SendKeystrokes(pub String);
214
215#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema)]
216pub struct Reload {
217 pub binary_path: Option<PathBuf>,
218}
219
220action_as!(project_symbols, ToggleProjectSymbols as Toggle);
221
222#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema)]
223pub struct ToggleFileFinder {
224 #[serde(default)]
225 pub separate_history: bool,
226}
227
228impl_action_as!(file_finder, ToggleFileFinder as Toggle);
229
230impl_actions!(
231 workspace,
232 [
233 ActivatePane,
234 ActivatePaneInDirection,
235 CloseAllItemsAndPanes,
236 CloseInactiveTabsAndPanes,
237 MoveItemToPane,
238 MoveItemToPaneInDirection,
239 OpenTerminal,
240 Reload,
241 Save,
242 SaveAll,
243 SwapPaneInDirection,
244 SendKeystrokes,
245 ]
246);
247
248#[derive(PartialEq, Eq, Debug)]
249pub enum CloseIntent {
250 /// Quit the program entirely.
251 Quit,
252 /// Close a window.
253 CloseWindow,
254 /// Replace the workspace in an existing window.
255 ReplaceWindow,
256}
257
258#[derive(Clone)]
259pub struct Toast {
260 id: NotificationId,
261 msg: Cow<'static, str>,
262 autohide: bool,
263 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
264}
265
266impl Toast {
267 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
268 Toast {
269 id,
270 msg: msg.into(),
271 on_click: None,
272 autohide: false,
273 }
274 }
275
276 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
277 where
278 M: Into<Cow<'static, str>>,
279 F: Fn(&mut WindowContext) + 'static,
280 {
281 self.on_click = Some((message.into(), Arc::new(on_click)));
282 self
283 }
284
285 pub fn autohide(mut self) -> Self {
286 self.autohide = true;
287 self
288 }
289}
290
291impl PartialEq for Toast {
292 fn eq(&self, other: &Self) -> bool {
293 self.id == other.id
294 && self.msg == other.msg
295 && self.on_click.is_some() == other.on_click.is_some()
296 }
297}
298
299#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
300pub struct OpenTerminal {
301 pub working_directory: PathBuf,
302}
303
304#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
305pub struct WorkspaceId(i64);
306
307impl StaticColumnCount for WorkspaceId {}
308impl Bind for WorkspaceId {
309 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
310 self.0.bind(statement, start_index)
311 }
312}
313impl Column for WorkspaceId {
314 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
315 i64::column(statement, start_index)
316 .map(|(i, next_index)| (Self(i), next_index))
317 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
318 }
319}
320impl From<WorkspaceId> for i64 {
321 fn from(val: WorkspaceId) -> Self {
322 val.0
323 }
324}
325
326pub fn init_settings(cx: &mut AppContext) {
327 WorkspaceSettings::register(cx);
328 ItemSettings::register(cx);
329 PreviewTabsSettings::register(cx);
330 TabBarSettings::register(cx);
331}
332
333fn prompt_and_open_paths(
334 app_state: Arc<AppState>,
335 options: PathPromptOptions,
336 cx: &mut AppContext,
337) {
338 let paths = cx.prompt_for_paths(options);
339 cx.spawn(|cx| async move {
340 match paths.await.anyhow().and_then(|res| res) {
341 Ok(Some(paths)) => {
342 cx.update(|cx| {
343 open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
344 })
345 .ok();
346 }
347 Ok(None) => {}
348 Err(err) => {
349 util::log_err(&err);
350 cx.update(|cx| {
351 if let Some(workspace_window) = cx
352 .active_window()
353 .and_then(|window| window.downcast::<Workspace>())
354 {
355 workspace_window
356 .update(cx, |workspace, cx| {
357 workspace.show_portal_error(err.to_string(), cx);
358 })
359 .ok();
360 }
361 })
362 .ok();
363 }
364 }
365 })
366 .detach();
367}
368
369pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
370 init_settings(cx);
371 notifications::init(cx);
372 theme_preview::init(cx);
373
374 cx.on_action(Workspace::close_global);
375 cx.on_action(reload);
376
377 cx.on_action({
378 let app_state = Arc::downgrade(&app_state);
379 move |_: &Open, cx: &mut AppContext| {
380 if let Some(app_state) = app_state.upgrade() {
381 prompt_and_open_paths(
382 app_state,
383 PathPromptOptions {
384 files: true,
385 directories: true,
386 multiple: true,
387 },
388 cx,
389 );
390 }
391 }
392 });
393 cx.on_action({
394 let app_state = Arc::downgrade(&app_state);
395 move |_: &OpenFiles, cx: &mut AppContext| {
396 let directories = cx.can_select_mixed_files_and_dirs();
397 if let Some(app_state) = app_state.upgrade() {
398 prompt_and_open_paths(
399 app_state,
400 PathPromptOptions {
401 files: true,
402 directories,
403 multiple: true,
404 },
405 cx,
406 );
407 }
408 }
409 });
410}
411
412#[derive(Clone, Default, Deref, DerefMut)]
413struct ProjectItemOpeners(Vec<ProjectItemOpener>);
414
415type ProjectItemOpener = fn(
416 &Model<Project>,
417 &ProjectPath,
418 &mut WindowContext,
419)
420 -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
421
422type WorkspaceItemBuilder = Box<dyn FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
423
424impl Global for ProjectItemOpeners {}
425
426/// Registers a [ProjectItem] for the app. When opening a file, all the registered
427/// items will get a chance to open the file, starting from the project item that
428/// was added last.
429pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
430 let builders = cx.default_global::<ProjectItemOpeners>();
431 builders.push(|project, project_path, cx| {
432 let project_item = <I::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
433 let project = project.clone();
434 Some(cx.spawn(|cx| async move {
435 let project_item = project_item.await?;
436 let project_entry_id: Option<ProjectEntryId> =
437 project_item.read_with(&cx, project::ProjectItem::entry_id)?;
438 let build_workspace_item = Box::new(|cx: &mut ViewContext<Pane>| {
439 Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx)))
440 as Box<dyn ItemHandle>
441 }) as Box<_>;
442 Ok((project_entry_id, build_workspace_item))
443 }))
444 });
445}
446
447#[derive(Default)]
448pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
449
450struct FollowableViewDescriptor {
451 from_state_proto: fn(
452 View<Workspace>,
453 ViewId,
454 &mut Option<proto::view::Variant>,
455 &mut WindowContext,
456 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
457 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
458}
459
460impl Global for FollowableViewRegistry {}
461
462impl FollowableViewRegistry {
463 pub fn register<I: FollowableItem>(cx: &mut AppContext) {
464 cx.default_global::<Self>().0.insert(
465 TypeId::of::<I>(),
466 FollowableViewDescriptor {
467 from_state_proto: |workspace, id, state, cx| {
468 I::from_state_proto(workspace, id, state, cx).map(|task| {
469 cx.foreground_executor()
470 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
471 })
472 },
473 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
474 },
475 );
476 }
477
478 pub fn from_state_proto(
479 workspace: View<Workspace>,
480 view_id: ViewId,
481 mut state: Option<proto::view::Variant>,
482 cx: &mut WindowContext,
483 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
484 cx.update_default_global(|this: &mut Self, cx| {
485 this.0.values().find_map(|descriptor| {
486 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, cx)
487 })
488 })
489 }
490
491 pub fn to_followable_view(
492 view: impl Into<AnyView>,
493 cx: &AppContext,
494 ) -> Option<Box<dyn FollowableItemHandle>> {
495 let this = cx.try_global::<Self>()?;
496 let view = view.into();
497 let descriptor = this.0.get(&view.entity_type())?;
498 Some((descriptor.to_followable_view)(&view))
499 }
500}
501
502#[derive(Copy, Clone)]
503struct SerializableItemDescriptor {
504 deserialize: fn(
505 Model<Project>,
506 WeakView<Workspace>,
507 WorkspaceId,
508 ItemId,
509 &mut ViewContext<Pane>,
510 ) -> Task<Result<Box<dyn ItemHandle>>>,
511 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut WindowContext) -> Task<Result<()>>,
512 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
513}
514
515#[derive(Default)]
516struct SerializableItemRegistry {
517 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
518 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
519}
520
521impl Global for SerializableItemRegistry {}
522
523impl SerializableItemRegistry {
524 fn deserialize(
525 item_kind: &str,
526 project: Model<Project>,
527 workspace: WeakView<Workspace>,
528 workspace_id: WorkspaceId,
529 item_item: ItemId,
530 cx: &mut ViewContext<Pane>,
531 ) -> Task<Result<Box<dyn ItemHandle>>> {
532 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
533 return Task::ready(Err(anyhow!(
534 "cannot deserialize {}, descriptor not found",
535 item_kind
536 )));
537 };
538
539 (descriptor.deserialize)(project, workspace, workspace_id, item_item, cx)
540 }
541
542 fn cleanup(
543 item_kind: &str,
544 workspace_id: WorkspaceId,
545 loaded_items: Vec<ItemId>,
546 cx: &mut WindowContext,
547 ) -> Task<Result<()>> {
548 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
549 return Task::ready(Err(anyhow!(
550 "cannot cleanup {}, descriptor not found",
551 item_kind
552 )));
553 };
554
555 (descriptor.cleanup)(workspace_id, loaded_items, cx)
556 }
557
558 fn view_to_serializable_item_handle(
559 view: AnyView,
560 cx: &AppContext,
561 ) -> Option<Box<dyn SerializableItemHandle>> {
562 let this = cx.try_global::<Self>()?;
563 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
564 Some((descriptor.view_to_serializable_item)(view))
565 }
566
567 fn descriptor(item_kind: &str, cx: &AppContext) -> Option<SerializableItemDescriptor> {
568 let this = cx.try_global::<Self>()?;
569 this.descriptors_by_kind.get(item_kind).copied()
570 }
571}
572
573pub fn register_serializable_item<I: SerializableItem>(cx: &mut AppContext) {
574 let serialized_item_kind = I::serialized_item_kind();
575
576 let registry = cx.default_global::<SerializableItemRegistry>();
577 let descriptor = SerializableItemDescriptor {
578 deserialize: |project, workspace, workspace_id, item_id, cx| {
579 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
580 cx.foreground_executor()
581 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
582 },
583 cleanup: |workspace_id, loaded_items, cx| I::cleanup(workspace_id, loaded_items, cx),
584 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
585 };
586 registry
587 .descriptors_by_kind
588 .insert(Arc::from(serialized_item_kind), descriptor);
589 registry
590 .descriptors_by_type
591 .insert(TypeId::of::<I>(), descriptor);
592}
593
594pub struct AppState {
595 pub languages: Arc<LanguageRegistry>,
596 pub client: Arc<Client>,
597 pub user_store: Model<UserStore>,
598 pub workspace_store: Model<WorkspaceStore>,
599 pub fs: Arc<dyn fs::Fs>,
600 pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
601 pub node_runtime: NodeRuntime,
602 pub session: Model<AppSession>,
603}
604
605struct GlobalAppState(Weak<AppState>);
606
607impl Global for GlobalAppState {}
608
609pub struct WorkspaceStore {
610 workspaces: HashSet<WindowHandle<Workspace>>,
611 client: Arc<Client>,
612 _subscriptions: Vec<client::Subscription>,
613}
614
615#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
616struct Follower {
617 project_id: Option<u64>,
618 peer_id: PeerId,
619}
620
621impl AppState {
622 pub fn global(cx: &AppContext) -> Weak<Self> {
623 cx.global::<GlobalAppState>().0.clone()
624 }
625 pub fn try_global(cx: &AppContext) -> Option<Weak<Self>> {
626 cx.try_global::<GlobalAppState>()
627 .map(|state| state.0.clone())
628 }
629 pub fn set_global(state: Weak<AppState>, cx: &mut AppContext) {
630 cx.set_global(GlobalAppState(state));
631 }
632
633 #[cfg(any(test, feature = "test-support"))]
634 pub fn test(cx: &mut AppContext) -> Arc<Self> {
635 use node_runtime::NodeRuntime;
636 use session::Session;
637 use settings::SettingsStore;
638
639 if !cx.has_global::<SettingsStore>() {
640 let settings_store = SettingsStore::test(cx);
641 cx.set_global(settings_store);
642 }
643
644 let fs = fs::FakeFs::new(cx.background_executor().clone());
645 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
646 let clock = Arc::new(clock::FakeSystemClock::new());
647 let http_client = http_client::FakeHttpClient::with_404_response();
648 let client = Client::new(clock, http_client.clone(), cx);
649 let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
650 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
651 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
652
653 theme::init(theme::LoadThemes::JustBase, cx);
654 client::init(&client, cx);
655 crate::init_settings(cx);
656
657 Arc::new(Self {
658 client,
659 fs,
660 languages,
661 user_store,
662 workspace_store,
663 node_runtime: NodeRuntime::unavailable(),
664 build_window_options: |_, _| Default::default(),
665 session,
666 })
667 }
668}
669
670struct DelayedDebouncedEditAction {
671 task: Option<Task<()>>,
672 cancel_channel: Option<oneshot::Sender<()>>,
673}
674
675impl DelayedDebouncedEditAction {
676 fn new() -> DelayedDebouncedEditAction {
677 DelayedDebouncedEditAction {
678 task: None,
679 cancel_channel: None,
680 }
681 }
682
683 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
684 where
685 F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
686 {
687 if let Some(channel) = self.cancel_channel.take() {
688 _ = channel.send(());
689 }
690
691 let (sender, mut receiver) = oneshot::channel::<()>();
692 self.cancel_channel = Some(sender);
693
694 let previous_task = self.task.take();
695 self.task = Some(cx.spawn(move |workspace, mut cx| async move {
696 let mut timer = cx.background_executor().timer(delay).fuse();
697 if let Some(previous_task) = previous_task {
698 previous_task.await;
699 }
700
701 futures::select_biased! {
702 _ = receiver => return,
703 _ = timer => {}
704 }
705
706 if let Some(result) = workspace
707 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
708 .log_err()
709 {
710 result.await.log_err();
711 }
712 }));
713 }
714}
715
716pub enum Event {
717 PaneAdded(View<Pane>),
718 PaneRemoved,
719 ItemAdded {
720 item: Box<dyn ItemHandle>,
721 },
722 ItemRemoved,
723 ActiveItemChanged,
724 UserSavedItem {
725 pane: WeakView<Pane>,
726 item: Box<dyn WeakItemHandle>,
727 save_intent: SaveIntent,
728 },
729 ContactRequestedJoin(u64),
730 WorkspaceCreated(WeakView<Workspace>),
731 SpawnTask {
732 action: Box<SpawnInTerminal>,
733 },
734 OpenBundledFile {
735 text: Cow<'static, str>,
736 title: &'static str,
737 language: &'static str,
738 },
739 ZoomChanged,
740}
741
742#[derive(Debug)]
743pub enum OpenVisible {
744 All,
745 None,
746 OnlyFiles,
747 OnlyDirectories,
748}
749
750type PromptForNewPath = Box<
751 dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<ProjectPath>>,
752>;
753
754type PromptForOpenPath = Box<
755 dyn Fn(
756 &mut Workspace,
757 DirectoryLister,
758 &mut ViewContext<Workspace>,
759 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
760>;
761
762/// Collects everything project-related for a certain window opened.
763/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
764///
765/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
766/// The `Workspace` owns everybody's state and serves as a default, "global context",
767/// that can be used to register a global action to be triggered from any place in the window.
768pub struct Workspace {
769 weak_self: WeakView<Self>,
770 workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
771 zoomed: Option<AnyWeakView>,
772 previous_dock_drag_coordinates: Option<Point<Pixels>>,
773 zoomed_position: Option<DockPosition>,
774 center: PaneGroup,
775 left_dock: View<Dock>,
776 bottom_dock: View<Dock>,
777 right_dock: View<Dock>,
778 panes: Vec<View<Pane>>,
779 panes_by_item: HashMap<EntityId, WeakView<Pane>>,
780 active_pane: View<Pane>,
781 last_active_center_pane: Option<WeakView<Pane>>,
782 last_active_view_id: Option<proto::ViewId>,
783 status_bar: View<StatusBar>,
784 modal_layer: View<ModalLayer>,
785 titlebar_item: Option<AnyView>,
786 notifications: Vec<(NotificationId, AnyView)>,
787 project: Model<Project>,
788 follower_states: HashMap<PeerId, FollowerState>,
789 last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
790 window_edited: bool,
791 active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
792 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
793 database_id: Option<WorkspaceId>,
794 app_state: Arc<AppState>,
795 dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
796 _subscriptions: Vec<Subscription>,
797 _apply_leader_updates: Task<Result<()>>,
798 _observe_current_user: Task<Result<()>>,
799 _schedule_serialize: Option<Task<()>>,
800 pane_history_timestamp: Arc<AtomicUsize>,
801 bounds: Bounds<Pixels>,
802 centered_layout: bool,
803 bounds_save_task_queued: Option<Task<()>>,
804 on_prompt_for_new_path: Option<PromptForNewPath>,
805 on_prompt_for_open_path: Option<PromptForOpenPath>,
806 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
807 serialized_ssh_project: Option<SerializedSshProject>,
808 _items_serializer: Task<Result<()>>,
809 session_id: Option<String>,
810}
811
812impl EventEmitter<Event> for Workspace {}
813
814#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
815pub struct ViewId {
816 pub creator: PeerId,
817 pub id: u64,
818}
819
820pub struct FollowerState {
821 center_pane: View<Pane>,
822 dock_pane: Option<View<Pane>>,
823 active_view_id: Option<ViewId>,
824 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
825}
826
827struct FollowerView {
828 view: Box<dyn FollowableItemHandle>,
829 location: Option<proto::PanelId>,
830}
831
832impl Workspace {
833 const DEFAULT_PADDING: f32 = 0.2;
834 const MAX_PADDING: f32 = 0.4;
835
836 pub fn new(
837 workspace_id: Option<WorkspaceId>,
838 project: Model<Project>,
839 app_state: Arc<AppState>,
840 cx: &mut ViewContext<Self>,
841 ) -> Self {
842 cx.observe(&project, |_, _, cx| cx.notify()).detach();
843 cx.subscribe(&project, move |this, _, event, cx| {
844 match event {
845 project::Event::RemoteIdChanged(_) => {
846 this.update_window_title(cx);
847 }
848
849 project::Event::CollaboratorLeft(peer_id) => {
850 this.collaborator_left(*peer_id, cx);
851 }
852
853 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
854 this.update_window_title(cx);
855 this.serialize_workspace(cx);
856 }
857
858 project::Event::DisconnectedFromHost => {
859 this.update_window_edited(cx);
860 let leaders_to_unfollow =
861 this.follower_states.keys().copied().collect::<Vec<_>>();
862 for leader_id in leaders_to_unfollow {
863 this.unfollow(leader_id, cx);
864 }
865 }
866
867 project::Event::DisconnectedFromSshRemote => {
868 this.update_window_edited(cx);
869 }
870
871 project::Event::Closed => {
872 cx.remove_window();
873 }
874
875 project::Event::DeletedEntry(_, entry_id) => {
876 for pane in this.panes.iter() {
877 pane.update(cx, |pane, cx| {
878 pane.handle_deleted_project_item(*entry_id, cx)
879 });
880 }
881 }
882
883 project::Event::Toast {
884 notification_id,
885 message,
886 } => this.show_notification(
887 NotificationId::named(notification_id.clone()),
888 cx,
889 |cx| cx.new_view(|_| MessageNotification::new(message.clone())),
890 ),
891
892 project::Event::HideToast { notification_id } => {
893 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
894 }
895
896 project::Event::LanguageServerPrompt(request) => {
897 struct LanguageServerPrompt;
898
899 let mut hasher = DefaultHasher::new();
900 request.lsp_name.as_str().hash(&mut hasher);
901 let id = hasher.finish();
902
903 this.show_notification(
904 NotificationId::composite::<LanguageServerPrompt>(id as usize),
905 cx,
906 |cx| {
907 cx.new_view(|_| {
908 notifications::LanguageServerPrompt::new(request.clone())
909 })
910 },
911 );
912 }
913
914 _ => {}
915 }
916 cx.notify()
917 })
918 .detach();
919
920 cx.on_focus_lost(|this, cx| {
921 let focus_handle = this.focus_handle(cx);
922 cx.focus(&focus_handle);
923 })
924 .detach();
925
926 let weak_handle = cx.view().downgrade();
927 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
928
929 let center_pane = cx.new_view(|cx| {
930 let mut center_pane = Pane::new(
931 weak_handle.clone(),
932 project.clone(),
933 pane_history_timestamp.clone(),
934 None,
935 NewFile.boxed_clone(),
936 cx,
937 );
938 center_pane.set_can_split(Some(Arc::new(|_, _, _| true)));
939 center_pane
940 });
941 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
942
943 cx.focus_view(¢er_pane);
944 cx.emit(Event::PaneAdded(center_pane.clone()));
945
946 let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
947 app_state.workspace_store.update(cx, |store, _| {
948 store.workspaces.insert(window_handle);
949 });
950
951 let mut current_user = app_state.user_store.read(cx).watch_current_user();
952 let mut connection_status = app_state.client.status();
953 let _observe_current_user = cx.spawn(|this, mut cx| async move {
954 current_user.next().await;
955 connection_status.next().await;
956 let mut stream =
957 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
958
959 while stream.recv().await.is_some() {
960 this.update(&mut cx, |_, cx| cx.notify())?;
961 }
962 anyhow::Ok(())
963 });
964
965 // All leader updates are enqueued and then processed in a single task, so
966 // that each asynchronous operation can be run in order.
967 let (leader_updates_tx, mut leader_updates_rx) =
968 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
969 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
970 while let Some((leader_id, update)) = leader_updates_rx.next().await {
971 Self::process_leader_update(&this, leader_id, update, &mut cx)
972 .await
973 .log_err();
974 }
975
976 Ok(())
977 });
978
979 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
980
981 let left_dock = Dock::new(DockPosition::Left, cx);
982 let bottom_dock = Dock::new(DockPosition::Bottom, cx);
983 let right_dock = Dock::new(DockPosition::Right, cx);
984 let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
985 let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
986 let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
987 let status_bar = cx.new_view(|cx| {
988 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
989 status_bar.add_left_item(left_dock_buttons, cx);
990 status_bar.add_right_item(right_dock_buttons, cx);
991 status_bar.add_right_item(bottom_dock_buttons, cx);
992 status_bar
993 });
994
995 let modal_layer = cx.new_view(|_| ModalLayer::new());
996
997 let session_id = app_state.session.read(cx).id().to_owned();
998
999 let mut active_call = None;
1000 if let Some(call) = ActiveCall::try_global(cx) {
1001 let call = call.clone();
1002 let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
1003 active_call = Some((call, subscriptions));
1004 }
1005
1006 let (serializable_items_tx, serializable_items_rx) =
1007 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1008 let _items_serializer = cx.spawn(|this, mut cx| async move {
1009 Self::serialize_items(&this, serializable_items_rx, &mut cx).await
1010 });
1011
1012 let subscriptions = vec![
1013 cx.observe_window_activation(Self::on_window_activation_changed),
1014 cx.observe_window_bounds(move |this, cx| {
1015 if this.bounds_save_task_queued.is_some() {
1016 return;
1017 }
1018 this.bounds_save_task_queued = Some(cx.spawn(|this, mut cx| async move {
1019 cx.background_executor()
1020 .timer(Duration::from_millis(100))
1021 .await;
1022 this.update(&mut cx, |this, cx| {
1023 if let Some(display) = cx.display() {
1024 if let Ok(display_uuid) = display.uuid() {
1025 let window_bounds = cx.inner_window_bounds();
1026 if let Some(database_id) = workspace_id {
1027 cx.background_executor()
1028 .spawn(DB.set_window_open_status(
1029 database_id,
1030 SerializedWindowBounds(window_bounds),
1031 display_uuid,
1032 ))
1033 .detach_and_log_err(cx);
1034 }
1035 }
1036 }
1037 this.bounds_save_task_queued.take();
1038 })
1039 .ok();
1040 }));
1041 cx.notify();
1042 }),
1043 cx.observe_window_appearance(|_, cx| {
1044 let window_appearance = cx.appearance();
1045
1046 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1047
1048 ThemeSettings::reload_current_theme(cx);
1049 }),
1050 cx.on_release(|this, window, cx| {
1051 this.app_state.workspace_store.update(cx, |store, _| {
1052 let window = window.downcast::<Self>().unwrap();
1053 store.workspaces.remove(&window);
1054 })
1055 }),
1056 ];
1057
1058 cx.defer(|this, cx| {
1059 this.update_window_title(cx);
1060 this.show_initial_notifications(cx);
1061 });
1062 Workspace {
1063 weak_self: weak_handle.clone(),
1064 zoomed: None,
1065 zoomed_position: None,
1066 previous_dock_drag_coordinates: None,
1067 center: PaneGroup::new(center_pane.clone()),
1068 panes: vec![center_pane.clone()],
1069 panes_by_item: Default::default(),
1070 active_pane: center_pane.clone(),
1071 last_active_center_pane: Some(center_pane.downgrade()),
1072 last_active_view_id: None,
1073 status_bar,
1074 modal_layer,
1075 titlebar_item: None,
1076 notifications: Default::default(),
1077 left_dock,
1078 bottom_dock,
1079 right_dock,
1080 project: project.clone(),
1081 follower_states: Default::default(),
1082 last_leaders_by_pane: Default::default(),
1083 dispatching_keystrokes: Default::default(),
1084 window_edited: false,
1085 active_call,
1086 database_id: workspace_id,
1087 app_state,
1088 _observe_current_user,
1089 _apply_leader_updates,
1090 _schedule_serialize: None,
1091 leader_updates_tx,
1092 _subscriptions: subscriptions,
1093 pane_history_timestamp,
1094 workspace_actions: Default::default(),
1095 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1096 bounds: Default::default(),
1097 centered_layout: false,
1098 bounds_save_task_queued: None,
1099 on_prompt_for_new_path: None,
1100 on_prompt_for_open_path: None,
1101 serializable_items_tx,
1102 _items_serializer,
1103 session_id: Some(session_id),
1104 serialized_ssh_project: None,
1105 }
1106 }
1107
1108 pub fn new_local(
1109 abs_paths: Vec<PathBuf>,
1110 app_state: Arc<AppState>,
1111 requesting_window: Option<WindowHandle<Workspace>>,
1112 env: Option<HashMap<String, String>>,
1113 cx: &mut AppContext,
1114 ) -> Task<
1115 anyhow::Result<(
1116 WindowHandle<Workspace>,
1117 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
1118 )>,
1119 > {
1120 let project_handle = Project::local(
1121 app_state.client.clone(),
1122 app_state.node_runtime.clone(),
1123 app_state.user_store.clone(),
1124 app_state.languages.clone(),
1125 app_state.fs.clone(),
1126 env,
1127 cx,
1128 );
1129
1130 cx.spawn(|mut cx| async move {
1131 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1132 for path in abs_paths.into_iter() {
1133 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1134 paths_to_open.push(canonical)
1135 } else {
1136 paths_to_open.push(path)
1137 }
1138 }
1139
1140 let serialized_workspace: Option<SerializedWorkspace> =
1141 persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1142
1143 let workspace_location = serialized_workspace
1144 .as_ref()
1145 .map(|ws| &ws.location)
1146 .and_then(|loc| match loc {
1147 SerializedWorkspaceLocation::Local(_, order) => {
1148 Some((loc.sorted_paths(), order.order()))
1149 }
1150 _ => None,
1151 });
1152
1153 if let Some((paths, order)) = workspace_location {
1154 paths_to_open = paths.iter().cloned().collect();
1155
1156 if order.iter().enumerate().any(|(i, &j)| i != j) {
1157 project_handle
1158 .update(&mut cx, |project, cx| {
1159 project.set_worktrees_reordered(true, cx);
1160 })
1161 .log_err();
1162 }
1163 }
1164
1165 // Get project paths for all of the abs_paths
1166 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1167 Vec::with_capacity(paths_to_open.len());
1168 for path in paths_to_open.into_iter() {
1169 if let Some((_, project_entry)) = cx
1170 .update(|cx| {
1171 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1172 })?
1173 .await
1174 .log_err()
1175 {
1176 project_paths.push((path, Some(project_entry)));
1177 } else {
1178 project_paths.push((path, None));
1179 }
1180 }
1181
1182 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1183 serialized_workspace.id
1184 } else {
1185 DB.next_id().await.unwrap_or_else(|_| Default::default())
1186 };
1187
1188 let toolchains = DB.toolchains(workspace_id).await?;
1189 for (toolchain, worktree_id) in toolchains {
1190 project_handle
1191 .update(&mut cx, |this, cx| {
1192 this.activate_toolchain(worktree_id, toolchain, cx)
1193 })?
1194 .await;
1195 }
1196 let window = if let Some(window) = requesting_window {
1197 cx.update_window(window.into(), |_, cx| {
1198 cx.replace_root_view(|cx| {
1199 Workspace::new(
1200 Some(workspace_id),
1201 project_handle.clone(),
1202 app_state.clone(),
1203 cx,
1204 )
1205 });
1206 })?;
1207 window
1208 } else {
1209 let window_bounds_override = window_bounds_env_override();
1210
1211 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1212 (Some(WindowBounds::Windowed(bounds)), None)
1213 } else {
1214 let restorable_bounds = serialized_workspace
1215 .as_ref()
1216 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
1217 .or_else(|| {
1218 let (display, window_bounds) = DB.last_window().log_err()?;
1219 Some((display?, window_bounds?))
1220 });
1221
1222 if let Some((serialized_display, serialized_status)) = restorable_bounds {
1223 (Some(serialized_status.0), Some(serialized_display))
1224 } else {
1225 (None, None)
1226 }
1227 };
1228
1229 // Use the serialized workspace to construct the new window
1230 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
1231 options.window_bounds = window_bounds;
1232 let centered_layout = serialized_workspace
1233 .as_ref()
1234 .map(|w| w.centered_layout)
1235 .unwrap_or(false);
1236 cx.open_window(options, {
1237 let app_state = app_state.clone();
1238 let project_handle = project_handle.clone();
1239 move |cx| {
1240 cx.new_view(|cx| {
1241 let mut workspace =
1242 Workspace::new(Some(workspace_id), project_handle, app_state, cx);
1243 workspace.centered_layout = centered_layout;
1244 workspace
1245 })
1246 }
1247 })?
1248 };
1249
1250 notify_if_database_failed(window, &mut cx);
1251 let opened_items = window
1252 .update(&mut cx, |_workspace, cx| {
1253 open_items(serialized_workspace, project_paths, cx)
1254 })?
1255 .await
1256 .unwrap_or_default();
1257
1258 window
1259 .update(&mut cx, |_, cx| cx.activate_window())
1260 .log_err();
1261 Ok((window, opened_items))
1262 })
1263 }
1264
1265 pub fn weak_handle(&self) -> WeakView<Self> {
1266 self.weak_self.clone()
1267 }
1268
1269 pub fn left_dock(&self) -> &View<Dock> {
1270 &self.left_dock
1271 }
1272
1273 pub fn bottom_dock(&self) -> &View<Dock> {
1274 &self.bottom_dock
1275 }
1276
1277 pub fn right_dock(&self) -> &View<Dock> {
1278 &self.right_dock
1279 }
1280
1281 pub fn is_edited(&self) -> bool {
1282 self.window_edited
1283 }
1284
1285 pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
1286 let focus_handle = panel.focus_handle(cx);
1287 cx.on_focus_in(&focus_handle, Self::handle_panel_focused)
1288 .detach();
1289
1290 let dock = match panel.position(cx) {
1291 DockPosition::Left => &self.left_dock,
1292 DockPosition::Bottom => &self.bottom_dock,
1293 DockPosition::Right => &self.right_dock,
1294 };
1295
1296 dock.update(cx, |dock, cx| {
1297 dock.add_panel(panel, self.weak_self.clone(), cx)
1298 });
1299 }
1300
1301 pub fn status_bar(&self) -> &View<StatusBar> {
1302 &self.status_bar
1303 }
1304
1305 pub fn app_state(&self) -> &Arc<AppState> {
1306 &self.app_state
1307 }
1308
1309 pub fn user_store(&self) -> &Model<UserStore> {
1310 &self.app_state.user_store
1311 }
1312
1313 pub fn project(&self) -> &Model<Project> {
1314 &self.project
1315 }
1316
1317 pub fn recent_navigation_history_iter(
1318 &self,
1319 cx: &AppContext,
1320 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> {
1321 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1322 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1323 for pane in &self.panes {
1324 let pane = pane.read(cx);
1325 pane.nav_history()
1326 .for_each_entry(cx, |entry, (project_path, fs_path)| {
1327 if let Some(fs_path) = &fs_path {
1328 abs_paths_opened
1329 .entry(fs_path.clone())
1330 .or_default()
1331 .insert(project_path.clone());
1332 }
1333 let timestamp = entry.timestamp;
1334 match history.entry(project_path) {
1335 hash_map::Entry::Occupied(mut entry) => {
1336 let (_, old_timestamp) = entry.get();
1337 if ×tamp > old_timestamp {
1338 entry.insert((fs_path, timestamp));
1339 }
1340 }
1341 hash_map::Entry::Vacant(entry) => {
1342 entry.insert((fs_path, timestamp));
1343 }
1344 }
1345 });
1346 }
1347
1348 history
1349 .into_iter()
1350 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1351 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1352 .rev()
1353 .filter(move |(history_path, abs_path)| {
1354 let latest_project_path_opened = abs_path
1355 .as_ref()
1356 .and_then(|abs_path| abs_paths_opened.get(abs_path))
1357 .and_then(|project_paths| {
1358 project_paths
1359 .iter()
1360 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1361 });
1362
1363 match latest_project_path_opened {
1364 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1365 None => true,
1366 }
1367 })
1368 }
1369
1370 pub fn recent_navigation_history(
1371 &self,
1372 limit: Option<usize>,
1373 cx: &AppContext,
1374 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1375 self.recent_navigation_history_iter(cx)
1376 .take(limit.unwrap_or(usize::MAX))
1377 .collect()
1378 }
1379
1380 fn navigate_history(
1381 &mut self,
1382 pane: WeakView<Pane>,
1383 mode: NavigationMode,
1384 cx: &mut ViewContext<Workspace>,
1385 ) -> Task<Result<()>> {
1386 let to_load = if let Some(pane) = pane.upgrade() {
1387 pane.update(cx, |pane, cx| {
1388 pane.focus(cx);
1389 loop {
1390 // Retrieve the weak item handle from the history.
1391 let entry = pane.nav_history_mut().pop(mode, cx)?;
1392
1393 // If the item is still present in this pane, then activate it.
1394 if let Some(index) = entry
1395 .item
1396 .upgrade()
1397 .and_then(|v| pane.index_for_item(v.as_ref()))
1398 {
1399 let prev_active_item_index = pane.active_item_index();
1400 pane.nav_history_mut().set_mode(mode);
1401 pane.activate_item(index, true, true, cx);
1402 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1403
1404 let mut navigated = prev_active_item_index != pane.active_item_index();
1405 if let Some(data) = entry.data {
1406 navigated |= pane.active_item()?.navigate(data, cx);
1407 }
1408
1409 if navigated {
1410 break None;
1411 }
1412 } else {
1413 // If the item is no longer present in this pane, then retrieve its
1414 // path info in order to reopen it.
1415 break pane
1416 .nav_history()
1417 .path_for_item(entry.item.id())
1418 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
1419 }
1420 }
1421 })
1422 } else {
1423 None
1424 };
1425
1426 if let Some((project_path, abs_path, entry)) = to_load {
1427 // If the item was no longer present, then load it again from its previous path, first try the local path
1428 let open_by_project_path = self.load_path(project_path.clone(), cx);
1429
1430 cx.spawn(|workspace, mut cx| async move {
1431 let open_by_project_path = open_by_project_path.await;
1432 let mut navigated = false;
1433 match open_by_project_path
1434 .with_context(|| format!("Navigating to {project_path:?}"))
1435 {
1436 Ok((project_entry_id, build_item)) => {
1437 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1438 pane.nav_history_mut().set_mode(mode);
1439 pane.active_item().map(|p| p.item_id())
1440 })?;
1441
1442 pane.update(&mut cx, |pane, cx| {
1443 let item = pane.open_item(
1444 project_entry_id,
1445 true,
1446 entry.is_preview,
1447 None,
1448 cx,
1449 build_item,
1450 );
1451 navigated |= Some(item.item_id()) != prev_active_item_id;
1452 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1453 if let Some(data) = entry.data {
1454 navigated |= item.navigate(data, cx);
1455 }
1456 })?;
1457 }
1458 Err(open_by_project_path_e) => {
1459 // Fall back to opening by abs path, in case an external file was opened and closed,
1460 // and its worktree is now dropped
1461 if let Some(abs_path) = abs_path {
1462 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1463 pane.nav_history_mut().set_mode(mode);
1464 pane.active_item().map(|p| p.item_id())
1465 })?;
1466 let open_by_abs_path = workspace.update(&mut cx, |workspace, cx| {
1467 workspace.open_abs_path(abs_path.clone(), false, cx)
1468 })?;
1469 match open_by_abs_path
1470 .await
1471 .with_context(|| format!("Navigating to {abs_path:?}"))
1472 {
1473 Ok(item) => {
1474 pane.update(&mut cx, |pane, cx| {
1475 navigated |= Some(item.item_id()) != prev_active_item_id;
1476 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1477 if let Some(data) = entry.data {
1478 navigated |= item.navigate(data, cx);
1479 }
1480 })?;
1481 }
1482 Err(open_by_abs_path_e) => {
1483 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
1484 }
1485 }
1486 }
1487 }
1488 }
1489
1490 if !navigated {
1491 workspace
1492 .update(&mut cx, |workspace, cx| {
1493 Self::navigate_history(workspace, pane, mode, cx)
1494 })?
1495 .await?;
1496 }
1497
1498 Ok(())
1499 })
1500 } else {
1501 Task::ready(Ok(()))
1502 }
1503 }
1504
1505 pub fn go_back(
1506 &mut self,
1507 pane: WeakView<Pane>,
1508 cx: &mut ViewContext<Workspace>,
1509 ) -> Task<Result<()>> {
1510 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1511 }
1512
1513 pub fn go_forward(
1514 &mut self,
1515 pane: WeakView<Pane>,
1516 cx: &mut ViewContext<Workspace>,
1517 ) -> Task<Result<()>> {
1518 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1519 }
1520
1521 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1522 self.navigate_history(
1523 self.active_pane().downgrade(),
1524 NavigationMode::ReopeningClosedItem,
1525 cx,
1526 )
1527 }
1528
1529 pub fn client(&self) -> &Arc<Client> {
1530 &self.app_state.client
1531 }
1532
1533 pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
1534 self.titlebar_item = Some(item);
1535 cx.notify();
1536 }
1537
1538 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
1539 self.on_prompt_for_new_path = Some(prompt)
1540 }
1541
1542 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
1543 self.on_prompt_for_open_path = Some(prompt)
1544 }
1545
1546 pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
1547 self.serialized_ssh_project.clone()
1548 }
1549
1550 pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
1551 self.serialized_ssh_project = Some(serialized_ssh_project);
1552 }
1553
1554 pub fn prompt_for_open_path(
1555 &mut self,
1556 path_prompt_options: PathPromptOptions,
1557 lister: DirectoryLister,
1558 cx: &mut ViewContext<Self>,
1559 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
1560 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
1561 let prompt = self.on_prompt_for_open_path.take().unwrap();
1562 let rx = prompt(self, lister, cx);
1563 self.on_prompt_for_open_path = Some(prompt);
1564 rx
1565 } else {
1566 let (tx, rx) = oneshot::channel();
1567 let abs_path = cx.prompt_for_paths(path_prompt_options);
1568
1569 cx.spawn(|this, mut cx| async move {
1570 let Ok(result) = abs_path.await else {
1571 return Ok(());
1572 };
1573
1574 match result {
1575 Ok(result) => {
1576 tx.send(result).log_err();
1577 }
1578 Err(err) => {
1579 let rx = this.update(&mut cx, |this, cx| {
1580 this.show_portal_error(err.to_string(), cx);
1581 let prompt = this.on_prompt_for_open_path.take().unwrap();
1582 let rx = prompt(this, lister, cx);
1583 this.on_prompt_for_open_path = Some(prompt);
1584 rx
1585 })?;
1586 if let Ok(path) = rx.await {
1587 tx.send(path).log_err();
1588 }
1589 }
1590 };
1591 anyhow::Ok(())
1592 })
1593 .detach();
1594
1595 rx
1596 }
1597 }
1598
1599 pub fn prompt_for_new_path(
1600 &mut self,
1601 cx: &mut ViewContext<Self>,
1602 ) -> oneshot::Receiver<Option<ProjectPath>> {
1603 if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
1604 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
1605 {
1606 let prompt = self.on_prompt_for_new_path.take().unwrap();
1607 let rx = prompt(self, cx);
1608 self.on_prompt_for_new_path = Some(prompt);
1609 rx
1610 } else {
1611 let start_abs_path = self
1612 .project
1613 .update(cx, |project, cx| {
1614 let worktree = project.visible_worktrees(cx).next()?;
1615 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1616 })
1617 .unwrap_or_else(|| Path::new("").into());
1618
1619 let (tx, rx) = oneshot::channel();
1620 let abs_path = cx.prompt_for_new_path(&start_abs_path);
1621 cx.spawn(|this, mut cx| async move {
1622 let abs_path = match abs_path.await? {
1623 Ok(path) => path,
1624 Err(err) => {
1625 let rx = this.update(&mut cx, |this, cx| {
1626 this.show_portal_error(err.to_string(), cx);
1627
1628 let prompt = this.on_prompt_for_new_path.take().unwrap();
1629 let rx = prompt(this, cx);
1630 this.on_prompt_for_new_path = Some(prompt);
1631 rx
1632 })?;
1633 if let Ok(path) = rx.await {
1634 tx.send(path).log_err();
1635 }
1636 return anyhow::Ok(());
1637 }
1638 };
1639
1640 let project_path = abs_path.and_then(|abs_path| {
1641 this.update(&mut cx, |this, cx| {
1642 this.project.update(cx, |project, cx| {
1643 project.find_or_create_worktree(abs_path, true, cx)
1644 })
1645 })
1646 .ok()
1647 });
1648
1649 if let Some(project_path) = project_path {
1650 let (worktree, path) = project_path.await?;
1651 let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
1652 tx.send(Some(ProjectPath {
1653 worktree_id,
1654 path: path.into(),
1655 }))
1656 .ok();
1657 } else {
1658 tx.send(None).ok();
1659 }
1660 anyhow::Ok(())
1661 })
1662 .detach_and_log_err(cx);
1663
1664 rx
1665 }
1666 }
1667
1668 pub fn titlebar_item(&self) -> Option<AnyView> {
1669 self.titlebar_item.clone()
1670 }
1671
1672 /// Call the given callback with a workspace whose project is local.
1673 ///
1674 /// If the given workspace has a local project, then it will be passed
1675 /// to the callback. Otherwise, a new empty window will be created.
1676 pub fn with_local_workspace<T, F>(
1677 &mut self,
1678 cx: &mut ViewContext<Self>,
1679 callback: F,
1680 ) -> Task<Result<T>>
1681 where
1682 T: 'static,
1683 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1684 {
1685 if self.project.read(cx).is_local() {
1686 Task::ready(Ok(callback(self, cx)))
1687 } else {
1688 let env = self.project.read(cx).cli_environment(cx);
1689 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
1690 cx.spawn(|_vh, mut cx| async move {
1691 let (workspace, _) = task.await?;
1692 workspace.update(&mut cx, callback)
1693 })
1694 }
1695 }
1696
1697 pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
1698 self.project.read(cx).worktrees(cx)
1699 }
1700
1701 pub fn visible_worktrees<'a>(
1702 &self,
1703 cx: &'a AppContext,
1704 ) -> impl 'a + Iterator<Item = Model<Worktree>> {
1705 self.project.read(cx).visible_worktrees(cx)
1706 }
1707
1708 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1709 let futures = self
1710 .worktrees(cx)
1711 .filter_map(|worktree| worktree.read(cx).as_local())
1712 .map(|worktree| worktree.scan_complete())
1713 .collect::<Vec<_>>();
1714 async move {
1715 for future in futures {
1716 future.await;
1717 }
1718 }
1719 }
1720
1721 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1722 cx.defer(|cx| {
1723 cx.windows().iter().find(|window| {
1724 window
1725 .update(cx, |_, cx| {
1726 if cx.is_window_active() {
1727 //This can only get called when the window's project connection has been lost
1728 //so we don't need to prompt the user for anything and instead just close the window
1729 cx.remove_window();
1730 true
1731 } else {
1732 false
1733 }
1734 })
1735 .unwrap_or(false)
1736 });
1737 });
1738 }
1739
1740 pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
1741 let prepare = self.prepare_to_close(CloseIntent::CloseWindow, cx);
1742 let window = cx.window_handle();
1743 cx.spawn(|_, mut cx| async move {
1744 if prepare.await? {
1745 window.update(&mut cx, |_, cx| {
1746 cx.remove_window();
1747 })?;
1748 }
1749 anyhow::Ok(())
1750 })
1751 .detach_and_log_err(cx)
1752 }
1753
1754 pub fn move_focused_panel_to_next_position(
1755 &mut self,
1756 _: &MoveFocusedPanelToNextPosition,
1757 cx: &mut ViewContext<Self>,
1758 ) {
1759 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1760 let active_dock = docks
1761 .into_iter()
1762 .find(|dock| dock.focus_handle(cx).contains_focused(cx));
1763
1764 if let Some(dock) = active_dock {
1765 dock.update(cx, |dock, cx| {
1766 let active_panel = dock
1767 .active_panel()
1768 .filter(|panel| panel.focus_handle(cx).contains_focused(cx));
1769
1770 if let Some(panel) = active_panel {
1771 panel.move_to_next_position(cx);
1772 }
1773 })
1774 }
1775 }
1776
1777 pub fn prepare_to_close(
1778 &mut self,
1779 close_intent: CloseIntent,
1780 cx: &mut ViewContext<Self>,
1781 ) -> Task<Result<bool>> {
1782 let active_call = self.active_call().cloned();
1783 let window = cx.window_handle();
1784
1785 // On Linux and Windows, closing the last window should restore the last workspace.
1786 let save_last_workspace = cfg!(not(target_os = "macos"))
1787 && close_intent != CloseIntent::ReplaceWindow
1788 && cx.windows().len() == 1;
1789
1790 cx.spawn(|this, mut cx| async move {
1791 let workspace_count = (*cx).update(|cx| {
1792 cx.windows()
1793 .iter()
1794 .filter(|window| window.downcast::<Workspace>().is_some())
1795 .count()
1796 })?;
1797
1798 if let Some(active_call) = active_call {
1799 if close_intent != CloseIntent::Quit
1800 && workspace_count == 1
1801 && active_call.read_with(&cx, |call, _| call.room().is_some())?
1802 {
1803 let answer = window.update(&mut cx, |_, cx| {
1804 cx.prompt(
1805 PromptLevel::Warning,
1806 "Do you want to leave the current call?",
1807 None,
1808 &["Close window and hang up", "Cancel"],
1809 )
1810 })?;
1811
1812 if answer.await.log_err() == Some(1) {
1813 return anyhow::Ok(false);
1814 } else {
1815 active_call
1816 .update(&mut cx, |call, cx| call.hang_up(cx))?
1817 .await
1818 .log_err();
1819 }
1820 }
1821 }
1822
1823 let save_result = this
1824 .update(&mut cx, |this, cx| {
1825 this.save_all_internal(SaveIntent::Close, cx)
1826 })?
1827 .await;
1828
1829 // If we're not quitting, but closing, we remove the workspace from
1830 // the current session.
1831 if close_intent != CloseIntent::Quit
1832 && !save_last_workspace
1833 && save_result.as_ref().map_or(false, |&res| res)
1834 {
1835 this.update(&mut cx, |this, cx| this.remove_from_session(cx))?
1836 .await;
1837 }
1838
1839 save_result
1840 })
1841 }
1842
1843 fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
1844 self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
1845 .detach_and_log_err(cx);
1846 }
1847
1848 fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext<Self>) {
1849 let mut state = self.dispatching_keystrokes.borrow_mut();
1850 if !state.0.insert(action.0.clone()) {
1851 cx.propagate();
1852 return;
1853 }
1854 let mut keystrokes: Vec<Keystroke> = action
1855 .0
1856 .split(' ')
1857 .flat_map(|k| Keystroke::parse(k).log_err())
1858 .collect();
1859 keystrokes.reverse();
1860
1861 state.1.append(&mut keystrokes);
1862 drop(state);
1863
1864 let keystrokes = self.dispatching_keystrokes.clone();
1865 cx.window_context()
1866 .spawn(|mut cx| async move {
1867 // limit to 100 keystrokes to avoid infinite recursion.
1868 for _ in 0..100 {
1869 let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
1870 keystrokes.borrow_mut().0.clear();
1871 return Ok(());
1872 };
1873 cx.update(|cx| {
1874 let focused = cx.focused();
1875 cx.dispatch_keystroke(keystroke.clone());
1876 if cx.focused() != focused {
1877 // dispatch_keystroke may cause the focus to change.
1878 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
1879 // And we need that to happen before the next keystroke to keep vim mode happy...
1880 // (Note that the tests always do this implicitly, so you must manually test with something like:
1881 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
1882 // )
1883 cx.draw();
1884 }
1885 })?;
1886 }
1887
1888 *keystrokes.borrow_mut() = Default::default();
1889 Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
1890 })
1891 .detach_and_log_err(cx);
1892 }
1893
1894 fn save_all_internal(
1895 &mut self,
1896 mut save_intent: SaveIntent,
1897 cx: &mut ViewContext<Self>,
1898 ) -> Task<Result<bool>> {
1899 if self.project.read(cx).is_disconnected(cx) {
1900 return Task::ready(Ok(true));
1901 }
1902 let dirty_items = self
1903 .panes
1904 .iter()
1905 .flat_map(|pane| {
1906 pane.read(cx).items().filter_map(|item| {
1907 if item.is_dirty(cx) {
1908 item.tab_description(0, cx);
1909 Some((pane.downgrade(), item.boxed_clone()))
1910 } else {
1911 None
1912 }
1913 })
1914 })
1915 .collect::<Vec<_>>();
1916
1917 let project = self.project.clone();
1918 cx.spawn(|workspace, mut cx| async move {
1919 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
1920 let (serialize_tasks, remaining_dirty_items) =
1921 workspace.update(&mut cx, |workspace, cx| {
1922 let mut remaining_dirty_items = Vec::new();
1923 let mut serialize_tasks = Vec::new();
1924 for (pane, item) in dirty_items {
1925 if let Some(task) = item
1926 .to_serializable_item_handle(cx)
1927 .and_then(|handle| handle.serialize(workspace, true, cx))
1928 {
1929 serialize_tasks.push(task);
1930 } else {
1931 remaining_dirty_items.push((pane, item));
1932 }
1933 }
1934 (serialize_tasks, remaining_dirty_items)
1935 })?;
1936
1937 futures::future::try_join_all(serialize_tasks).await?;
1938
1939 if remaining_dirty_items.len() > 1 {
1940 let answer = workspace.update(&mut cx, |_, cx| {
1941 let (prompt, detail) = Pane::file_names_for_prompt(
1942 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
1943 remaining_dirty_items.len(),
1944 cx,
1945 );
1946 cx.prompt(
1947 PromptLevel::Warning,
1948 &prompt,
1949 Some(&detail),
1950 &["Save all", "Discard all", "Cancel"],
1951 )
1952 })?;
1953 match answer.await.log_err() {
1954 Some(0) => save_intent = SaveIntent::SaveAll,
1955 Some(1) => save_intent = SaveIntent::Skip,
1956 _ => {}
1957 }
1958 }
1959
1960 remaining_dirty_items
1961 } else {
1962 dirty_items
1963 };
1964
1965 for (pane, item) in dirty_items {
1966 let (singleton, project_entry_ids) =
1967 cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
1968 if singleton || !project_entry_ids.is_empty() {
1969 if let Some(ix) =
1970 pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
1971 {
1972 if !Pane::save_item(
1973 project.clone(),
1974 &pane,
1975 ix,
1976 &*item,
1977 save_intent,
1978 &mut cx,
1979 )
1980 .await?
1981 {
1982 return Ok(false);
1983 }
1984 }
1985 }
1986 }
1987 Ok(true)
1988 })
1989 }
1990
1991 pub fn open_workspace_for_paths(
1992 &mut self,
1993 replace_current_window: bool,
1994 paths: Vec<PathBuf>,
1995 cx: &mut ViewContext<Self>,
1996 ) -> Task<Result<()>> {
1997 let window = cx.window_handle().downcast::<Self>();
1998 let is_remote = self.project.read(cx).is_via_collab();
1999 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2000 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2001
2002 let window_to_replace = if replace_current_window {
2003 window
2004 } else if is_remote || has_worktree || has_dirty_items {
2005 None
2006 } else {
2007 window
2008 };
2009 let app_state = self.app_state.clone();
2010
2011 cx.spawn(|_, mut cx| async move {
2012 cx.update(|cx| {
2013 open_paths(
2014 &paths,
2015 app_state,
2016 OpenOptions {
2017 replace_window: window_to_replace,
2018 ..Default::default()
2019 },
2020 cx,
2021 )
2022 })?
2023 .await?;
2024 Ok(())
2025 })
2026 }
2027
2028 #[allow(clippy::type_complexity)]
2029 pub fn open_paths(
2030 &mut self,
2031 mut abs_paths: Vec<PathBuf>,
2032 visible: OpenVisible,
2033 pane: Option<WeakView<Pane>>,
2034 cx: &mut ViewContext<Self>,
2035 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2036 log::info!("open paths {abs_paths:?}");
2037
2038 let fs = self.app_state.fs.clone();
2039
2040 // Sort the paths to ensure we add worktrees for parents before their children.
2041 abs_paths.sort_unstable();
2042 cx.spawn(move |this, mut cx| async move {
2043 let mut tasks = Vec::with_capacity(abs_paths.len());
2044
2045 for abs_path in &abs_paths {
2046 let visible = match visible {
2047 OpenVisible::All => Some(true),
2048 OpenVisible::None => Some(false),
2049 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
2050 Some(Some(metadata)) => Some(!metadata.is_dir),
2051 Some(None) => Some(true),
2052 None => None,
2053 },
2054 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
2055 Some(Some(metadata)) => Some(metadata.is_dir),
2056 Some(None) => Some(false),
2057 None => None,
2058 },
2059 };
2060 let project_path = match visible {
2061 Some(visible) => match this
2062 .update(&mut cx, |this, cx| {
2063 Workspace::project_path_for_path(
2064 this.project.clone(),
2065 abs_path,
2066 visible,
2067 cx,
2068 )
2069 })
2070 .log_err()
2071 {
2072 Some(project_path) => project_path.await.log_err(),
2073 None => None,
2074 },
2075 None => None,
2076 };
2077
2078 let this = this.clone();
2079 let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
2080 let fs = fs.clone();
2081 let pane = pane.clone();
2082 let task = cx.spawn(move |mut cx| async move {
2083 let (worktree, project_path) = project_path?;
2084 if fs.is_dir(&abs_path).await {
2085 this.update(&mut cx, |workspace, cx| {
2086 let worktree = worktree.read(cx);
2087 let worktree_abs_path = worktree.abs_path();
2088 let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
2089 worktree.root_entry()
2090 } else {
2091 abs_path
2092 .strip_prefix(worktree_abs_path.as_ref())
2093 .ok()
2094 .and_then(|relative_path| {
2095 worktree.entry_for_path(relative_path)
2096 })
2097 }
2098 .map(|entry| entry.id);
2099 if let Some(entry_id) = entry_id {
2100 workspace.project.update(cx, |_, cx| {
2101 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
2102 })
2103 }
2104 })
2105 .log_err()?;
2106 None
2107 } else {
2108 Some(
2109 this.update(&mut cx, |this, cx| {
2110 this.open_path(project_path, pane, true, cx)
2111 })
2112 .log_err()?
2113 .await,
2114 )
2115 }
2116 });
2117 tasks.push(task);
2118 }
2119
2120 futures::future::join_all(tasks).await
2121 })
2122 }
2123
2124 pub fn open_resolved_path(
2125 &mut self,
2126 path: ResolvedPath,
2127 cx: &mut ViewContext<Self>,
2128 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2129 match path {
2130 ResolvedPath::ProjectPath { project_path, .. } => {
2131 self.open_path(project_path, None, true, cx)
2132 }
2133 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(path, false, cx),
2134 }
2135 }
2136
2137 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
2138 let project = self.project.read(cx);
2139 if project.is_via_collab() {
2140 self.show_error(
2141 &anyhow!("You cannot add folders to someone else's project"),
2142 cx,
2143 );
2144 return;
2145 }
2146 let paths = self.prompt_for_open_path(
2147 PathPromptOptions {
2148 files: false,
2149 directories: true,
2150 multiple: true,
2151 },
2152 DirectoryLister::Project(self.project.clone()),
2153 cx,
2154 );
2155 cx.spawn(|this, mut cx| async move {
2156 if let Some(paths) = paths.await.log_err().flatten() {
2157 let results = this
2158 .update(&mut cx, |this, cx| {
2159 this.open_paths(paths, OpenVisible::All, None, cx)
2160 })?
2161 .await;
2162 for result in results.into_iter().flatten() {
2163 result.log_err();
2164 }
2165 }
2166 anyhow::Ok(())
2167 })
2168 .detach_and_log_err(cx);
2169 }
2170
2171 pub fn project_path_for_path(
2172 project: Model<Project>,
2173 abs_path: &Path,
2174 visible: bool,
2175 cx: &mut AppContext,
2176 ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
2177 let entry = project.update(cx, |project, cx| {
2178 project.find_or_create_worktree(abs_path, visible, cx)
2179 });
2180 cx.spawn(|mut cx| async move {
2181 let (worktree, path) = entry.await?;
2182 let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
2183 Ok((
2184 worktree,
2185 ProjectPath {
2186 worktree_id,
2187 path: path.into(),
2188 },
2189 ))
2190 })
2191 }
2192
2193 pub fn items<'a>(
2194 &'a self,
2195 cx: &'a AppContext,
2196 ) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
2197 self.panes.iter().flat_map(|pane| pane.read(cx).items())
2198 }
2199
2200 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
2201 self.items_of_type(cx).max_by_key(|item| item.item_id())
2202 }
2203
2204 pub fn items_of_type<'a, T: Item>(
2205 &'a self,
2206 cx: &'a AppContext,
2207 ) -> impl 'a + Iterator<Item = View<T>> {
2208 self.panes
2209 .iter()
2210 .flat_map(|pane| pane.read(cx).items_of_type())
2211 }
2212
2213 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
2214 self.active_pane().read(cx).active_item()
2215 }
2216
2217 pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
2218 let item = self.active_item(cx)?;
2219 item.to_any().downcast::<I>().ok()
2220 }
2221
2222 fn active_project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
2223 self.active_item(cx).and_then(|item| item.project_path(cx))
2224 }
2225
2226 pub fn save_active_item(
2227 &mut self,
2228 save_intent: SaveIntent,
2229 cx: &mut WindowContext,
2230 ) -> Task<Result<()>> {
2231 let project = self.project.clone();
2232 let pane = self.active_pane();
2233 let item_ix = pane.read(cx).active_item_index();
2234 let item = pane.read(cx).active_item();
2235 let pane = pane.downgrade();
2236
2237 cx.spawn(|mut cx| async move {
2238 if let Some(item) = item {
2239 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
2240 .await
2241 .map(|_| ())
2242 } else {
2243 Ok(())
2244 }
2245 })
2246 }
2247
2248 pub fn close_inactive_items_and_panes(
2249 &mut self,
2250 action: &CloseInactiveTabsAndPanes,
2251 cx: &mut ViewContext<Self>,
2252 ) {
2253 if let Some(task) =
2254 self.close_all_internal(true, action.save_intent.unwrap_or(SaveIntent::Close), cx)
2255 {
2256 task.detach_and_log_err(cx)
2257 }
2258 }
2259
2260 pub fn close_all_items_and_panes(
2261 &mut self,
2262 action: &CloseAllItemsAndPanes,
2263 cx: &mut ViewContext<Self>,
2264 ) {
2265 if let Some(task) =
2266 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
2267 {
2268 task.detach_and_log_err(cx)
2269 }
2270 }
2271
2272 fn close_all_internal(
2273 &mut self,
2274 retain_active_pane: bool,
2275 save_intent: SaveIntent,
2276 cx: &mut ViewContext<Self>,
2277 ) -> Option<Task<Result<()>>> {
2278 let current_pane = self.active_pane();
2279
2280 let mut tasks = Vec::new();
2281
2282 if retain_active_pane {
2283 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
2284 pane.close_inactive_items(
2285 &CloseInactiveItems {
2286 save_intent: None,
2287 close_pinned: false,
2288 },
2289 cx,
2290 )
2291 }) {
2292 tasks.push(current_pane_close);
2293 };
2294 }
2295
2296 for pane in self.panes() {
2297 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
2298 continue;
2299 }
2300
2301 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
2302 pane.close_all_items(
2303 &CloseAllItems {
2304 save_intent: Some(save_intent),
2305 close_pinned: false,
2306 },
2307 cx,
2308 )
2309 }) {
2310 tasks.push(close_pane_items)
2311 }
2312 }
2313
2314 if tasks.is_empty() {
2315 None
2316 } else {
2317 Some(cx.spawn(|_, _| async move {
2318 for task in tasks {
2319 task.await?
2320 }
2321 Ok(())
2322 }))
2323 }
2324 }
2325
2326 pub fn is_dock_at_position_open(
2327 &self,
2328 position: DockPosition,
2329 cx: &mut ViewContext<Self>,
2330 ) -> bool {
2331 let dock = match position {
2332 DockPosition::Left => &self.left_dock,
2333 DockPosition::Bottom => &self.bottom_dock,
2334 DockPosition::Right => &self.right_dock,
2335 };
2336 dock.read(cx).is_open()
2337 }
2338
2339 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
2340 let dock = match dock_side {
2341 DockPosition::Left => &self.left_dock,
2342 DockPosition::Bottom => &self.bottom_dock,
2343 DockPosition::Right => &self.right_dock,
2344 };
2345 let mut focus_center = false;
2346 let mut reveal_dock = false;
2347 dock.update(cx, |dock, cx| {
2348 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
2349 let was_visible = dock.is_open() && !other_is_zoomed;
2350 dock.set_open(!was_visible, cx);
2351
2352 if dock.active_panel().is_none() && dock.panels_len() > 0 {
2353 dock.activate_panel(0, cx);
2354 }
2355
2356 if let Some(active_panel) = dock.active_panel() {
2357 if was_visible {
2358 if active_panel.focus_handle(cx).contains_focused(cx) {
2359 focus_center = true;
2360 }
2361 } else {
2362 let focus_handle = &active_panel.focus_handle(cx);
2363 cx.focus(focus_handle);
2364 reveal_dock = true;
2365 }
2366 }
2367 });
2368
2369 if reveal_dock {
2370 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
2371 }
2372
2373 if focus_center {
2374 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2375 }
2376
2377 cx.notify();
2378 self.serialize_workspace(cx);
2379 }
2380
2381 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
2382 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
2383
2384 for dock in docks {
2385 dock.update(cx, |dock, cx| {
2386 dock.set_open(false, cx);
2387 });
2388 }
2389
2390 cx.focus_self();
2391 cx.notify();
2392 self.serialize_workspace(cx);
2393 }
2394
2395 /// Transfer focus to the panel of the given type.
2396 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
2397 let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
2398 panel.to_any().downcast().ok()
2399 }
2400
2401 /// Focus the panel of the given type if it isn't already focused. If it is
2402 /// already focused, then transfer focus back to the workspace center.
2403 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2404 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
2405 !panel.focus_handle(cx).contains_focused(cx)
2406 });
2407 }
2408
2409 pub fn activate_panel_for_proto_id(
2410 &mut self,
2411 panel_id: PanelId,
2412 cx: &mut ViewContext<Self>,
2413 ) -> Option<Arc<dyn PanelHandle>> {
2414 let mut panel = None;
2415 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2416 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2417 panel = dock.update(cx, |dock, cx| {
2418 dock.activate_panel(panel_index, cx);
2419 dock.set_open(true, cx);
2420 dock.active_panel().cloned()
2421 });
2422 break;
2423 }
2424 }
2425
2426 if panel.is_some() {
2427 cx.notify();
2428 self.serialize_workspace(cx);
2429 }
2430
2431 panel
2432 }
2433
2434 /// Focus or unfocus the given panel type, depending on the given callback.
2435 fn focus_or_unfocus_panel<T: Panel>(
2436 &mut self,
2437 cx: &mut ViewContext<Self>,
2438 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
2439 ) -> Option<Arc<dyn PanelHandle>> {
2440 let mut result_panel = None;
2441 let mut serialize = false;
2442 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2443 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2444 let mut focus_center = false;
2445 let panel = dock.update(cx, |dock, cx| {
2446 dock.activate_panel(panel_index, cx);
2447
2448 let panel = dock.active_panel().cloned();
2449 if let Some(panel) = panel.as_ref() {
2450 if should_focus(&**panel, cx) {
2451 dock.set_open(true, cx);
2452 panel.focus_handle(cx).focus(cx);
2453 } else {
2454 focus_center = true;
2455 }
2456 }
2457 panel
2458 });
2459
2460 if focus_center {
2461 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2462 }
2463
2464 result_panel = panel;
2465 serialize = true;
2466 break;
2467 }
2468 }
2469
2470 if serialize {
2471 self.serialize_workspace(cx);
2472 }
2473
2474 cx.notify();
2475 result_panel
2476 }
2477
2478 /// Open the panel of the given type
2479 pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2480 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2481 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2482 dock.update(cx, |dock, cx| {
2483 dock.activate_panel(panel_index, cx);
2484 dock.set_open(true, cx);
2485 });
2486 }
2487 }
2488 }
2489
2490 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
2491 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2492 .iter()
2493 .find_map(|dock| dock.read(cx).panel::<T>())
2494 }
2495
2496 fn dismiss_zoomed_items_to_reveal(
2497 &mut self,
2498 dock_to_reveal: Option<DockPosition>,
2499 cx: &mut ViewContext<Self>,
2500 ) {
2501 // If a center pane is zoomed, unzoom it.
2502 for pane in &self.panes {
2503 if pane != &self.active_pane || dock_to_reveal.is_some() {
2504 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2505 }
2506 }
2507
2508 // If another dock is zoomed, hide it.
2509 let mut focus_center = false;
2510 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2511 dock.update(cx, |dock, cx| {
2512 if Some(dock.position()) != dock_to_reveal {
2513 if let Some(panel) = dock.active_panel() {
2514 if panel.is_zoomed(cx) {
2515 focus_center |= panel.focus_handle(cx).contains_focused(cx);
2516 dock.set_open(false, cx);
2517 }
2518 }
2519 }
2520 });
2521 }
2522
2523 if focus_center {
2524 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2525 }
2526
2527 if self.zoomed_position != dock_to_reveal {
2528 self.zoomed = None;
2529 self.zoomed_position = None;
2530 cx.emit(Event::ZoomChanged);
2531 }
2532
2533 cx.notify();
2534 }
2535
2536 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2537 let pane = cx.new_view(|cx| {
2538 let mut pane = Pane::new(
2539 self.weak_handle(),
2540 self.project.clone(),
2541 self.pane_history_timestamp.clone(),
2542 None,
2543 NewFile.boxed_clone(),
2544 cx,
2545 );
2546 pane.set_can_split(Some(Arc::new(|_, _, _| true)));
2547 pane
2548 });
2549 cx.subscribe(&pane, Self::handle_pane_event).detach();
2550 self.panes.push(pane.clone());
2551 cx.focus_view(&pane);
2552 cx.emit(Event::PaneAdded(pane.clone()));
2553 pane
2554 }
2555
2556 pub fn add_item_to_center(
2557 &mut self,
2558 item: Box<dyn ItemHandle>,
2559 cx: &mut ViewContext<Self>,
2560 ) -> bool {
2561 if let Some(center_pane) = self.last_active_center_pane.clone() {
2562 if let Some(center_pane) = center_pane.upgrade() {
2563 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2564 true
2565 } else {
2566 false
2567 }
2568 } else {
2569 false
2570 }
2571 }
2572
2573 pub fn add_item_to_active_pane(
2574 &mut self,
2575 item: Box<dyn ItemHandle>,
2576 destination_index: Option<usize>,
2577 focus_item: bool,
2578 cx: &mut WindowContext,
2579 ) {
2580 self.add_item(
2581 self.active_pane.clone(),
2582 item,
2583 destination_index,
2584 false,
2585 focus_item,
2586 cx,
2587 )
2588 }
2589
2590 pub fn add_item(
2591 &mut self,
2592 pane: View<Pane>,
2593 item: Box<dyn ItemHandle>,
2594 destination_index: Option<usize>,
2595 activate_pane: bool,
2596 focus_item: bool,
2597 cx: &mut WindowContext,
2598 ) {
2599 if let Some(text) = item.telemetry_event_text(cx) {
2600 self.client()
2601 .telemetry()
2602 .report_app_event(format!("{}: open", text));
2603 }
2604
2605 pane.update(cx, |pane, cx| {
2606 pane.add_item(item, activate_pane, focus_item, destination_index, cx)
2607 });
2608 }
2609
2610 pub fn split_item(
2611 &mut self,
2612 split_direction: SplitDirection,
2613 item: Box<dyn ItemHandle>,
2614 cx: &mut ViewContext<Self>,
2615 ) {
2616 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2617 self.add_item(new_pane, item, None, true, true, cx);
2618 }
2619
2620 pub fn open_abs_path(
2621 &mut self,
2622 abs_path: PathBuf,
2623 visible: bool,
2624 cx: &mut ViewContext<Self>,
2625 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2626 cx.spawn(|workspace, mut cx| async move {
2627 let open_paths_task_result = workspace
2628 .update(&mut cx, |workspace, cx| {
2629 workspace.open_paths(
2630 vec![abs_path.clone()],
2631 if visible {
2632 OpenVisible::All
2633 } else {
2634 OpenVisible::None
2635 },
2636 None,
2637 cx,
2638 )
2639 })
2640 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2641 .await;
2642 anyhow::ensure!(
2643 open_paths_task_result.len() == 1,
2644 "open abs path {abs_path:?} task returned incorrect number of results"
2645 );
2646 match open_paths_task_result
2647 .into_iter()
2648 .next()
2649 .expect("ensured single task result")
2650 {
2651 Some(open_result) => {
2652 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2653 }
2654 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2655 }
2656 })
2657 }
2658
2659 pub fn split_abs_path(
2660 &mut self,
2661 abs_path: PathBuf,
2662 visible: bool,
2663 cx: &mut ViewContext<Self>,
2664 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2665 let project_path_task =
2666 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2667 cx.spawn(|this, mut cx| async move {
2668 let (_, path) = project_path_task.await?;
2669 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2670 .await
2671 })
2672 }
2673
2674 pub fn open_path(
2675 &mut self,
2676 path: impl Into<ProjectPath>,
2677 pane: Option<WeakView<Pane>>,
2678 focus_item: bool,
2679 cx: &mut WindowContext,
2680 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2681 self.open_path_preview(path, pane, focus_item, false, cx)
2682 }
2683
2684 pub fn open_path_preview(
2685 &mut self,
2686 path: impl Into<ProjectPath>,
2687 pane: Option<WeakView<Pane>>,
2688 focus_item: bool,
2689 allow_preview: bool,
2690 cx: &mut WindowContext,
2691 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2692 let pane = pane.unwrap_or_else(|| {
2693 self.last_active_center_pane.clone().unwrap_or_else(|| {
2694 self.panes
2695 .first()
2696 .expect("There must be an active pane")
2697 .downgrade()
2698 })
2699 });
2700
2701 let task = self.load_path(path.into(), cx);
2702 cx.spawn(move |mut cx| async move {
2703 let (project_entry_id, build_item) = task.await?;
2704 pane.update(&mut cx, |pane, cx| {
2705 pane.open_item(
2706 project_entry_id,
2707 focus_item,
2708 allow_preview,
2709 None,
2710 cx,
2711 build_item,
2712 )
2713 })
2714 })
2715 }
2716
2717 pub fn split_path(
2718 &mut self,
2719 path: impl Into<ProjectPath>,
2720 cx: &mut ViewContext<Self>,
2721 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2722 self.split_path_preview(path, false, None, cx)
2723 }
2724
2725 pub fn split_path_preview(
2726 &mut self,
2727 path: impl Into<ProjectPath>,
2728 allow_preview: bool,
2729 split_direction: Option<SplitDirection>,
2730 cx: &mut ViewContext<Self>,
2731 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2732 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2733 self.panes
2734 .first()
2735 .expect("There must be an active pane")
2736 .downgrade()
2737 });
2738
2739 if let Member::Pane(center_pane) = &self.center.root {
2740 if center_pane.read(cx).items_len() == 0 {
2741 return self.open_path(path, Some(pane), true, cx);
2742 }
2743 }
2744
2745 let task = self.load_path(path.into(), cx);
2746 cx.spawn(|this, mut cx| async move {
2747 let (project_entry_id, build_item) = task.await?;
2748 this.update(&mut cx, move |this, cx| -> Option<_> {
2749 let pane = pane.upgrade()?;
2750 let new_pane =
2751 this.split_pane(pane, split_direction.unwrap_or(SplitDirection::Right), cx);
2752 new_pane.update(cx, |new_pane, cx| {
2753 Some(new_pane.open_item(
2754 project_entry_id,
2755 true,
2756 allow_preview,
2757 None,
2758 cx,
2759 build_item,
2760 ))
2761 })
2762 })
2763 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2764 })
2765 }
2766
2767 fn load_path(
2768 &mut self,
2769 path: ProjectPath,
2770 cx: &mut WindowContext,
2771 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2772 let project = self.project().clone();
2773 let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2774 let Some(open_project_item) = project_item_builders
2775 .iter()
2776 .rev()
2777 .find_map(|open_project_item| open_project_item(&project, &path, cx))
2778 else {
2779 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2780 };
2781 open_project_item
2782 }
2783
2784 pub fn find_project_item<T>(
2785 &self,
2786 pane: &View<Pane>,
2787 project_item: &Model<T::Item>,
2788 cx: &AppContext,
2789 ) -> Option<View<T>>
2790 where
2791 T: ProjectItem,
2792 {
2793 use project::ProjectItem as _;
2794 let project_item = project_item.read(cx);
2795 let entry_id = project_item.entry_id(cx);
2796 let project_path = project_item.project_path(cx);
2797
2798 let mut item = None;
2799 if let Some(entry_id) = entry_id {
2800 item = pane.read(cx).item_for_entry(entry_id, cx);
2801 }
2802 if item.is_none() {
2803 if let Some(project_path) = project_path {
2804 item = pane.read(cx).item_for_path(project_path, cx);
2805 }
2806 }
2807
2808 item.and_then(|item| item.downcast::<T>())
2809 }
2810
2811 pub fn is_project_item_open<T>(
2812 &self,
2813 pane: &View<Pane>,
2814 project_item: &Model<T::Item>,
2815 cx: &AppContext,
2816 ) -> bool
2817 where
2818 T: ProjectItem,
2819 {
2820 self.find_project_item::<T>(pane, project_item, cx)
2821 .is_some()
2822 }
2823
2824 pub fn open_project_item<T>(
2825 &mut self,
2826 pane: View<Pane>,
2827 project_item: Model<T::Item>,
2828 activate_pane: bool,
2829 focus_item: bool,
2830 cx: &mut ViewContext<Self>,
2831 ) -> View<T>
2832 where
2833 T: ProjectItem,
2834 {
2835 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
2836 self.activate_item(&item, activate_pane, focus_item, cx);
2837 return item;
2838 }
2839
2840 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2841 let item_id = item.item_id();
2842 let mut destination_index = None;
2843 pane.update(cx, |pane, cx| {
2844 if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2845 if let Some(preview_item_id) = pane.preview_item_id() {
2846 if preview_item_id != item_id {
2847 destination_index = pane.close_current_preview_item(cx);
2848 }
2849 }
2850 }
2851 pane.set_preview_item_id(Some(item.item_id()), cx)
2852 });
2853
2854 self.add_item(
2855 pane,
2856 Box::new(item.clone()),
2857 destination_index,
2858 activate_pane,
2859 focus_item,
2860 cx,
2861 );
2862 item
2863 }
2864
2865 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2866 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2867 self.active_pane.update(cx, |pane, cx| {
2868 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2869 });
2870 }
2871 }
2872
2873 pub fn activate_item(
2874 &mut self,
2875 item: &dyn ItemHandle,
2876 activate_pane: bool,
2877 focus_item: bool,
2878 cx: &mut WindowContext,
2879 ) -> bool {
2880 let result = self.panes.iter().find_map(|pane| {
2881 pane.read(cx)
2882 .index_for_item(item)
2883 .map(|ix| (pane.clone(), ix))
2884 });
2885 if let Some((pane, ix)) = result {
2886 pane.update(cx, |pane, cx| {
2887 pane.activate_item(ix, activate_pane, focus_item, cx)
2888 });
2889 true
2890 } else {
2891 false
2892 }
2893 }
2894
2895 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2896 let panes = self.center.panes();
2897 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2898 cx.focus_view(&pane);
2899 } else {
2900 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2901 }
2902 }
2903
2904 fn move_item_to_pane_at_index(&mut self, action: &MoveItemToPane, cx: &mut ViewContext<Self>) {
2905 let Some(&target_pane) = self.center.panes().get(action.destination) else {
2906 return;
2907 };
2908 move_active_item(&self.active_pane, target_pane, action.focus, true, cx);
2909 }
2910
2911 pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2912 let panes = self.center.panes();
2913 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2914 let next_ix = (ix + 1) % panes.len();
2915 let next_pane = panes[next_ix].clone();
2916 cx.focus_view(&next_pane);
2917 }
2918 }
2919
2920 pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2921 let panes = self.center.panes();
2922 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2923 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2924 let prev_pane = panes[prev_ix].clone();
2925 cx.focus_view(&prev_pane);
2926 }
2927 }
2928
2929 pub fn activate_pane_in_direction(
2930 &mut self,
2931 direction: SplitDirection,
2932 cx: &mut WindowContext,
2933 ) {
2934 use ActivateInDirectionTarget as Target;
2935 enum Origin {
2936 LeftDock,
2937 RightDock,
2938 BottomDock,
2939 Center,
2940 }
2941
2942 let origin: Origin = [
2943 (&self.left_dock, Origin::LeftDock),
2944 (&self.right_dock, Origin::RightDock),
2945 (&self.bottom_dock, Origin::BottomDock),
2946 ]
2947 .into_iter()
2948 .find_map(|(dock, origin)| {
2949 if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2950 Some(origin)
2951 } else {
2952 None
2953 }
2954 })
2955 .unwrap_or(Origin::Center);
2956
2957 let get_last_active_pane = || {
2958 let pane = self
2959 .last_active_center_pane
2960 .clone()
2961 .unwrap_or_else(|| {
2962 self.panes
2963 .first()
2964 .expect("There must be an active pane")
2965 .downgrade()
2966 })
2967 .upgrade()?;
2968 (pane.read(cx).items_len() != 0).then_some(pane)
2969 };
2970
2971 let try_dock =
2972 |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2973
2974 let target = match (origin, direction) {
2975 // We're in the center, so we first try to go to a different pane,
2976 // otherwise try to go to a dock.
2977 (Origin::Center, direction) => {
2978 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2979 Some(Target::Pane(pane))
2980 } else {
2981 match direction {
2982 SplitDirection::Up => None,
2983 SplitDirection::Down => try_dock(&self.bottom_dock),
2984 SplitDirection::Left => try_dock(&self.left_dock),
2985 SplitDirection::Right => try_dock(&self.right_dock),
2986 }
2987 }
2988 }
2989
2990 (Origin::LeftDock, SplitDirection::Right) => {
2991 if let Some(last_active_pane) = get_last_active_pane() {
2992 Some(Target::Pane(last_active_pane))
2993 } else {
2994 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2995 }
2996 }
2997
2998 (Origin::LeftDock, SplitDirection::Down)
2999 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
3000
3001 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
3002 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
3003 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
3004
3005 (Origin::RightDock, SplitDirection::Left) => {
3006 if let Some(last_active_pane) = get_last_active_pane() {
3007 Some(Target::Pane(last_active_pane))
3008 } else {
3009 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
3010 }
3011 }
3012
3013 _ => None,
3014 };
3015
3016 match target {
3017 Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
3018 Some(ActivateInDirectionTarget::Dock(dock)) => {
3019 // Defer this to avoid a panic when the dock's active panel is already on the stack.
3020 cx.defer(move |cx| {
3021 let dock = dock.read(cx);
3022 if let Some(panel) = dock.active_panel() {
3023 panel.focus_handle(cx).focus(cx);
3024 } else {
3025 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
3026 }
3027 })
3028 }
3029 None => {}
3030 }
3031 }
3032
3033 pub fn move_item_to_pane_in_direction(
3034 &mut self,
3035 action: &MoveItemToPaneInDirection,
3036 cx: &mut WindowContext,
3037 ) {
3038 if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
3039 move_active_item(&self.active_pane, &destination, action.focus, true, cx);
3040 }
3041 }
3042
3043 pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
3044 self.center.bounding_box_for_pane(pane)
3045 }
3046
3047 pub fn find_pane_in_direction(
3048 &mut self,
3049 direction: SplitDirection,
3050 cx: &WindowContext,
3051 ) -> Option<View<Pane>> {
3052 self.center
3053 .find_pane_in_direction(&self.active_pane, direction, cx)
3054 .cloned()
3055 }
3056
3057 pub fn swap_pane_in_direction(
3058 &mut self,
3059 direction: SplitDirection,
3060 cx: &mut ViewContext<Self>,
3061 ) {
3062 if let Some(to) = self.find_pane_in_direction(direction, cx) {
3063 self.center.swap(&self.active_pane, &to);
3064 cx.notify();
3065 }
3066 }
3067
3068 pub fn resize_pane(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut ViewContext<Self>) {
3069 self.center
3070 .resize(&self.active_pane, axis, amount, &self.bounds);
3071 cx.notify();
3072 }
3073
3074 pub fn reset_pane_sizes(&mut self, cx: &mut ViewContext<Self>) {
3075 self.center.reset_pane_sizes();
3076 cx.notify();
3077 }
3078
3079 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3080 // This is explicitly hoisted out of the following check for pane identity as
3081 // terminal panel panes are not registered as a center panes.
3082 self.status_bar.update(cx, |status_bar, cx| {
3083 status_bar.set_active_pane(&pane, cx);
3084 });
3085 if self.active_pane != pane {
3086 self.set_active_pane(&pane, cx);
3087 }
3088
3089 if self.last_active_center_pane.is_none() {
3090 self.last_active_center_pane = Some(pane.downgrade());
3091 }
3092
3093 self.dismiss_zoomed_items_to_reveal(None, cx);
3094 if pane.read(cx).is_zoomed() {
3095 self.zoomed = Some(pane.downgrade().into());
3096 } else {
3097 self.zoomed = None;
3098 }
3099 self.zoomed_position = None;
3100 cx.emit(Event::ZoomChanged);
3101 self.update_active_view_for_followers(cx);
3102 pane.model.update(cx, |pane, _| {
3103 pane.track_alternate_file_items();
3104 });
3105
3106 cx.notify();
3107 }
3108
3109 fn set_active_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) {
3110 self.active_pane = pane.clone();
3111 self.active_item_path_changed(cx);
3112 self.last_active_center_pane = Some(pane.downgrade());
3113 }
3114
3115 fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
3116 self.update_active_view_for_followers(cx);
3117 }
3118
3119 fn handle_pane_event(
3120 &mut self,
3121 pane: View<Pane>,
3122 event: &pane::Event,
3123 cx: &mut ViewContext<Self>,
3124 ) {
3125 let mut serialize_workspace = true;
3126 match event {
3127 pane::Event::AddItem { item } => {
3128 item.added_to_pane(self, pane, cx);
3129 cx.emit(Event::ItemAdded {
3130 item: item.boxed_clone(),
3131 });
3132 }
3133 pane::Event::Split(direction) => {
3134 self.split_and_clone(pane, *direction, cx);
3135 }
3136 pane::Event::JoinIntoNext => {
3137 self.join_pane_into_next(pane, cx);
3138 }
3139 pane::Event::JoinAll => {
3140 self.join_all_panes(cx);
3141 }
3142 pane::Event::Remove { focus_on_pane } => {
3143 self.remove_pane(pane, focus_on_pane.clone(), cx);
3144 }
3145 pane::Event::ActivateItem {
3146 local,
3147 focus_changed,
3148 } => {
3149 cx.on_next_frame(|_, cx| {
3150 cx.invalidate_character_coordinates();
3151 });
3152
3153 pane.model.update(cx, |pane, _| {
3154 pane.track_alternate_file_items();
3155 });
3156 if *local {
3157 self.unfollow_in_pane(&pane, cx);
3158 }
3159 if &pane == self.active_pane() {
3160 self.active_item_path_changed(cx);
3161 self.update_active_view_for_followers(cx);
3162 }
3163 serialize_workspace = *focus_changed || &pane != self.active_pane();
3164 }
3165 pane::Event::UserSavedItem { item, save_intent } => {
3166 cx.emit(Event::UserSavedItem {
3167 pane: pane.downgrade(),
3168 item: item.boxed_clone(),
3169 save_intent: *save_intent,
3170 });
3171 serialize_workspace = false;
3172 }
3173 pane::Event::ChangeItemTitle => {
3174 if pane == self.active_pane {
3175 self.active_item_path_changed(cx);
3176 }
3177 self.update_window_edited(cx);
3178 serialize_workspace = false;
3179 }
3180 pane::Event::RemoveItem { .. } => {}
3181 pane::Event::RemovedItem { item_id } => {
3182 cx.emit(Event::ActiveItemChanged);
3183 self.update_window_edited(cx);
3184 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3185 if entry.get().entity_id() == pane.entity_id() {
3186 entry.remove();
3187 }
3188 }
3189 }
3190 pane::Event::Focus => {
3191 cx.on_next_frame(|_, cx| {
3192 cx.invalidate_character_coordinates();
3193 });
3194 self.handle_pane_focused(pane.clone(), cx);
3195 }
3196 pane::Event::ZoomIn => {
3197 if pane == self.active_pane {
3198 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3199 if pane.read(cx).has_focus(cx) {
3200 self.zoomed = Some(pane.downgrade().into());
3201 self.zoomed_position = None;
3202 cx.emit(Event::ZoomChanged);
3203 }
3204 cx.notify();
3205 }
3206 }
3207 pane::Event::ZoomOut => {
3208 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3209 if self.zoomed_position.is_none() {
3210 self.zoomed = None;
3211 cx.emit(Event::ZoomChanged);
3212 }
3213 cx.notify();
3214 }
3215 }
3216
3217 if serialize_workspace {
3218 self.serialize_workspace(cx);
3219 }
3220 }
3221
3222 pub fn unfollow_in_pane(
3223 &mut self,
3224 pane: &View<Pane>,
3225 cx: &mut ViewContext<Workspace>,
3226 ) -> Option<PeerId> {
3227 let leader_id = self.leader_for_pane(pane)?;
3228 self.unfollow(leader_id, cx);
3229 Some(leader_id)
3230 }
3231
3232 pub fn split_pane(
3233 &mut self,
3234 pane_to_split: View<Pane>,
3235 split_direction: SplitDirection,
3236 cx: &mut ViewContext<Self>,
3237 ) -> View<Pane> {
3238 let new_pane = self.add_pane(cx);
3239 self.center
3240 .split(&pane_to_split, &new_pane, split_direction)
3241 .unwrap();
3242 cx.notify();
3243 new_pane
3244 }
3245
3246 pub fn split_and_clone(
3247 &mut self,
3248 pane: View<Pane>,
3249 direction: SplitDirection,
3250 cx: &mut ViewContext<Self>,
3251 ) -> Option<View<Pane>> {
3252 let item = pane.read(cx).active_item()?;
3253 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3254 let new_pane = self.add_pane(cx);
3255 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3256 self.center.split(&pane, &new_pane, direction).unwrap();
3257 Some(new_pane)
3258 } else {
3259 None
3260 };
3261 cx.notify();
3262 maybe_pane_handle
3263 }
3264
3265 pub fn split_pane_with_item(
3266 &mut self,
3267 pane_to_split: WeakView<Pane>,
3268 split_direction: SplitDirection,
3269 from: WeakView<Pane>,
3270 item_id_to_move: EntityId,
3271 cx: &mut ViewContext<Self>,
3272 ) {
3273 let Some(pane_to_split) = pane_to_split.upgrade() else {
3274 return;
3275 };
3276 let Some(from) = from.upgrade() else {
3277 return;
3278 };
3279
3280 let new_pane = self.add_pane(cx);
3281 move_item(&from, &new_pane, item_id_to_move, 0, cx);
3282 self.center
3283 .split(&pane_to_split, &new_pane, split_direction)
3284 .unwrap();
3285 cx.notify();
3286 }
3287
3288 pub fn split_pane_with_project_entry(
3289 &mut self,
3290 pane_to_split: WeakView<Pane>,
3291 split_direction: SplitDirection,
3292 project_entry: ProjectEntryId,
3293 cx: &mut ViewContext<Self>,
3294 ) -> Option<Task<Result<()>>> {
3295 let pane_to_split = pane_to_split.upgrade()?;
3296 let new_pane = self.add_pane(cx);
3297 self.center
3298 .split(&pane_to_split, &new_pane, split_direction)
3299 .unwrap();
3300
3301 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3302 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3303 Some(cx.foreground_executor().spawn(async move {
3304 task.await?;
3305 Ok(())
3306 }))
3307 }
3308
3309 pub fn join_all_panes(&mut self, cx: &mut ViewContext<Self>) {
3310 let active_item = self.active_pane.read(cx).active_item();
3311 for pane in &self.panes {
3312 join_pane_into_active(&self.active_pane, pane, cx);
3313 }
3314 if let Some(active_item) = active_item {
3315 self.activate_item(active_item.as_ref(), true, true, cx);
3316 }
3317 cx.notify();
3318 }
3319
3320 pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3321 let next_pane = self
3322 .find_pane_in_direction(SplitDirection::Right, cx)
3323 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3324 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3325 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3326 let Some(next_pane) = next_pane else {
3327 return;
3328 };
3329 move_all_items(&pane, &next_pane, cx);
3330 cx.notify();
3331 }
3332
3333 fn remove_pane(
3334 &mut self,
3335 pane: View<Pane>,
3336 focus_on: Option<View<Pane>>,
3337 cx: &mut ViewContext<Self>,
3338 ) {
3339 if self.center.remove(&pane).unwrap() {
3340 self.force_remove_pane(&pane, &focus_on, cx);
3341 self.unfollow_in_pane(&pane, cx);
3342 self.last_leaders_by_pane.remove(&pane.downgrade());
3343 for removed_item in pane.read(cx).items() {
3344 self.panes_by_item.remove(&removed_item.item_id());
3345 }
3346
3347 cx.notify();
3348 } else {
3349 self.active_item_path_changed(cx);
3350 }
3351 cx.emit(Event::PaneRemoved);
3352 }
3353
3354 pub fn panes(&self) -> &[View<Pane>] {
3355 &self.panes
3356 }
3357
3358 pub fn active_pane(&self) -> &View<Pane> {
3359 &self.active_pane
3360 }
3361
3362 pub fn focused_pane(&self, cx: &WindowContext) -> View<Pane> {
3363 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3364 if dock.focus_handle(cx).contains_focused(cx) {
3365 if let Some(pane) = dock
3366 .read(cx)
3367 .active_panel()
3368 .and_then(|panel| panel.pane(cx))
3369 {
3370 return pane;
3371 }
3372 }
3373 }
3374 self.active_pane().clone()
3375 }
3376
3377 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3378 self.find_pane_in_direction(SplitDirection::Right, cx)
3379 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3380 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3381 .clone()
3382 }
3383
3384 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3385 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3386 weak_pane.upgrade()
3387 }
3388
3389 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3390 self.follower_states.retain(|leader_id, state| {
3391 if *leader_id == peer_id {
3392 for item in state.items_by_leader_view_id.values() {
3393 item.view.set_leader_peer_id(None, cx);
3394 }
3395 false
3396 } else {
3397 true
3398 }
3399 });
3400 cx.notify();
3401 }
3402
3403 pub fn start_following(
3404 &mut self,
3405 leader_id: PeerId,
3406 cx: &mut ViewContext<Self>,
3407 ) -> Option<Task<Result<()>>> {
3408 let pane = self.active_pane().clone();
3409
3410 self.last_leaders_by_pane
3411 .insert(pane.downgrade(), leader_id);
3412 self.unfollow(leader_id, cx);
3413 self.unfollow_in_pane(&pane, cx);
3414 self.follower_states.insert(
3415 leader_id,
3416 FollowerState {
3417 center_pane: pane.clone(),
3418 dock_pane: None,
3419 active_view_id: None,
3420 items_by_leader_view_id: Default::default(),
3421 },
3422 );
3423 cx.notify();
3424
3425 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3426 let project_id = self.project.read(cx).remote_id();
3427 let request = self.app_state.client.request(proto::Follow {
3428 room_id,
3429 project_id,
3430 leader_id: Some(leader_id),
3431 });
3432
3433 Some(cx.spawn(|this, mut cx| async move {
3434 let response = request.await?;
3435 this.update(&mut cx, |this, _| {
3436 let state = this
3437 .follower_states
3438 .get_mut(&leader_id)
3439 .ok_or_else(|| anyhow!("following interrupted"))?;
3440 state.active_view_id = response
3441 .active_view
3442 .as_ref()
3443 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3444 Ok::<_, anyhow::Error>(())
3445 })??;
3446 if let Some(view) = response.active_view {
3447 Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3448 }
3449 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3450 Ok(())
3451 }))
3452 }
3453
3454 pub fn follow_next_collaborator(
3455 &mut self,
3456 _: &FollowNextCollaborator,
3457 cx: &mut ViewContext<Self>,
3458 ) {
3459 let collaborators = self.project.read(cx).collaborators();
3460 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3461 let mut collaborators = collaborators.keys().copied();
3462 for peer_id in collaborators.by_ref() {
3463 if peer_id == leader_id {
3464 break;
3465 }
3466 }
3467 collaborators.next()
3468 } else if let Some(last_leader_id) =
3469 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3470 {
3471 if collaborators.contains_key(last_leader_id) {
3472 Some(*last_leader_id)
3473 } else {
3474 None
3475 }
3476 } else {
3477 None
3478 };
3479
3480 let pane = self.active_pane.clone();
3481 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3482 else {
3483 return;
3484 };
3485 if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3486 return;
3487 }
3488 if let Some(task) = self.start_following(leader_id, cx) {
3489 task.detach_and_log_err(cx)
3490 }
3491 }
3492
3493 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3494 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3495 return;
3496 };
3497 let room = room.read(cx);
3498 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3499 return;
3500 };
3501
3502 let project = self.project.read(cx);
3503
3504 let other_project_id = match remote_participant.location {
3505 call::ParticipantLocation::External => None,
3506 call::ParticipantLocation::UnsharedProject => None,
3507 call::ParticipantLocation::SharedProject { project_id } => {
3508 if Some(project_id) == project.remote_id() {
3509 None
3510 } else {
3511 Some(project_id)
3512 }
3513 }
3514 };
3515
3516 // if they are active in another project, follow there.
3517 if let Some(project_id) = other_project_id {
3518 let app_state = self.app_state.clone();
3519 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3520 .detach_and_log_err(cx);
3521 }
3522
3523 // if you're already following, find the right pane and focus it.
3524 if let Some(follower_state) = self.follower_states.get(&leader_id) {
3525 cx.focus_view(follower_state.pane());
3526 return;
3527 }
3528
3529 // Otherwise, follow.
3530 if let Some(task) = self.start_following(leader_id, cx) {
3531 task.detach_and_log_err(cx)
3532 }
3533 }
3534
3535 pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3536 cx.notify();
3537 let state = self.follower_states.remove(&leader_id)?;
3538 for (_, item) in state.items_by_leader_view_id {
3539 item.view.set_leader_peer_id(None, cx);
3540 }
3541
3542 let project_id = self.project.read(cx).remote_id();
3543 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3544 self.app_state
3545 .client
3546 .send(proto::Unfollow {
3547 room_id,
3548 project_id,
3549 leader_id: Some(leader_id),
3550 })
3551 .log_err();
3552
3553 Some(())
3554 }
3555
3556 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3557 self.follower_states.contains_key(&peer_id)
3558 }
3559
3560 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3561 cx.emit(Event::ActiveItemChanged);
3562 let active_entry = self.active_project_path(cx);
3563 self.project
3564 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3565
3566 self.update_window_title(cx);
3567 }
3568
3569 fn update_window_title(&mut self, cx: &mut WindowContext) {
3570 let project = self.project().read(cx);
3571 let mut title = String::new();
3572
3573 for (i, name) in project.worktree_root_names(cx).enumerate() {
3574 if i > 0 {
3575 title.push_str(", ");
3576 }
3577 title.push_str(name);
3578 }
3579
3580 if title.is_empty() {
3581 title = "empty project".to_string();
3582 }
3583
3584 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3585 let filename = path
3586 .path
3587 .file_name()
3588 .map(|s| s.to_string_lossy())
3589 .or_else(|| {
3590 Some(Cow::Borrowed(
3591 project
3592 .worktree_for_id(path.worktree_id, cx)?
3593 .read(cx)
3594 .root_name(),
3595 ))
3596 });
3597
3598 if let Some(filename) = filename {
3599 title.push_str(" — ");
3600 title.push_str(filename.as_ref());
3601 }
3602 }
3603
3604 if project.is_via_collab() {
3605 title.push_str(" ↙");
3606 } else if project.is_shared() {
3607 title.push_str(" ↗");
3608 }
3609
3610 cx.set_window_title(&title);
3611 }
3612
3613 fn update_window_edited(&mut self, cx: &mut WindowContext) {
3614 let is_edited = !self.project.read(cx).is_disconnected(cx)
3615 && self
3616 .items(cx)
3617 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3618 if is_edited != self.window_edited {
3619 self.window_edited = is_edited;
3620 cx.set_window_edited(self.window_edited)
3621 }
3622 }
3623
3624 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3625 if self.notifications.is_empty() {
3626 None
3627 } else {
3628 Some(
3629 div()
3630 .absolute()
3631 .right_3()
3632 .bottom_3()
3633 .w_112()
3634 .h_full()
3635 .flex()
3636 .flex_col()
3637 .justify_end()
3638 .gap_2()
3639 .children(
3640 self.notifications
3641 .iter()
3642 .map(|(_, notification)| notification.clone().into_any()),
3643 ),
3644 )
3645 }
3646 }
3647
3648 // RPC handlers
3649
3650 fn active_view_for_follower(
3651 &self,
3652 follower_project_id: Option<u64>,
3653 cx: &mut ViewContext<Self>,
3654 ) -> Option<proto::View> {
3655 let (item, panel_id) = self.active_item_for_followers(cx);
3656 let item = item?;
3657 let leader_id = self
3658 .pane_for(&*item)
3659 .and_then(|pane| self.leader_for_pane(&pane));
3660
3661 let item_handle = item.to_followable_item_handle(cx)?;
3662 let id = item_handle.remote_id(&self.app_state.client, cx)?;
3663 let variant = item_handle.to_state_proto(cx)?;
3664
3665 if item_handle.is_project_item(cx)
3666 && (follower_project_id.is_none()
3667 || follower_project_id != self.project.read(cx).remote_id())
3668 {
3669 return None;
3670 }
3671
3672 Some(proto::View {
3673 id: Some(id.to_proto()),
3674 leader_id,
3675 variant: Some(variant),
3676 panel_id: panel_id.map(|id| id as i32),
3677 })
3678 }
3679
3680 fn handle_follow(
3681 &mut self,
3682 follower_project_id: Option<u64>,
3683 cx: &mut ViewContext<Self>,
3684 ) -> proto::FollowResponse {
3685 let active_view = self.active_view_for_follower(follower_project_id, cx);
3686
3687 cx.notify();
3688 proto::FollowResponse {
3689 // TODO: Remove after version 0.145.x stabilizes.
3690 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3691 views: active_view.iter().cloned().collect(),
3692 active_view,
3693 }
3694 }
3695
3696 fn handle_update_followers(
3697 &mut self,
3698 leader_id: PeerId,
3699 message: proto::UpdateFollowers,
3700 _cx: &mut ViewContext<Self>,
3701 ) {
3702 self.leader_updates_tx
3703 .unbounded_send((leader_id, message))
3704 .ok();
3705 }
3706
3707 async fn process_leader_update(
3708 this: &WeakView<Self>,
3709 leader_id: PeerId,
3710 update: proto::UpdateFollowers,
3711 cx: &mut AsyncWindowContext,
3712 ) -> Result<()> {
3713 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3714 proto::update_followers::Variant::CreateView(view) => {
3715 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3716 let should_add_view = this.update(cx, |this, _| {
3717 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3718 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3719 } else {
3720 anyhow::Ok(false)
3721 }
3722 })??;
3723
3724 if should_add_view {
3725 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3726 }
3727 }
3728 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3729 let should_add_view = this.update(cx, |this, _| {
3730 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3731 state.active_view_id = update_active_view
3732 .view
3733 .as_ref()
3734 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3735
3736 if state.active_view_id.is_some_and(|view_id| {
3737 !state.items_by_leader_view_id.contains_key(&view_id)
3738 }) {
3739 anyhow::Ok(true)
3740 } else {
3741 anyhow::Ok(false)
3742 }
3743 } else {
3744 anyhow::Ok(false)
3745 }
3746 })??;
3747
3748 if should_add_view {
3749 if let Some(view) = update_active_view.view {
3750 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3751 }
3752 }
3753 }
3754 proto::update_followers::Variant::UpdateView(update_view) => {
3755 let variant = update_view
3756 .variant
3757 .ok_or_else(|| anyhow!("missing update view variant"))?;
3758 let id = update_view
3759 .id
3760 .ok_or_else(|| anyhow!("missing update view id"))?;
3761 let mut tasks = Vec::new();
3762 this.update(cx, |this, cx| {
3763 let project = this.project.clone();
3764 if let Some(state) = this.follower_states.get(&leader_id) {
3765 let view_id = ViewId::from_proto(id.clone())?;
3766 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3767 tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3768 }
3769 }
3770 anyhow::Ok(())
3771 })??;
3772 try_join_all(tasks).await.log_err();
3773 }
3774 }
3775 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3776 Ok(())
3777 }
3778
3779 async fn add_view_from_leader(
3780 this: WeakView<Self>,
3781 leader_id: PeerId,
3782 view: &proto::View,
3783 cx: &mut AsyncWindowContext,
3784 ) -> Result<()> {
3785 let this = this.upgrade().context("workspace dropped")?;
3786
3787 let Some(id) = view.id.clone() else {
3788 return Err(anyhow!("no id for view"));
3789 };
3790 let id = ViewId::from_proto(id)?;
3791 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
3792
3793 let pane = this.update(cx, |this, _cx| {
3794 let state = this
3795 .follower_states
3796 .get(&leader_id)
3797 .context("stopped following")?;
3798 anyhow::Ok(state.pane().clone())
3799 })??;
3800 let existing_item = pane.update(cx, |pane, cx| {
3801 let client = this.read(cx).client().clone();
3802 pane.items().find_map(|item| {
3803 let item = item.to_followable_item_handle(cx)?;
3804 if item.remote_id(&client, cx) == Some(id) {
3805 Some(item)
3806 } else {
3807 None
3808 }
3809 })
3810 })?;
3811 let item = if let Some(existing_item) = existing_item {
3812 existing_item
3813 } else {
3814 let variant = view.variant.clone();
3815 if variant.is_none() {
3816 Err(anyhow!("missing view variant"))?;
3817 }
3818
3819 let task = cx.update(|cx| {
3820 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3821 })?;
3822
3823 let Some(task) = task else {
3824 return Err(anyhow!(
3825 "failed to construct view from leader (maybe from a different version of zed?)"
3826 ));
3827 };
3828
3829 let mut new_item = task.await?;
3830 pane.update(cx, |pane, cx| {
3831 let mut item_to_remove = None;
3832 for (ix, item) in pane.items().enumerate() {
3833 if let Some(item) = item.to_followable_item_handle(cx) {
3834 match new_item.dedup(item.as_ref(), cx) {
3835 Some(item::Dedup::KeepExisting) => {
3836 new_item =
3837 item.boxed_clone().to_followable_item_handle(cx).unwrap();
3838 break;
3839 }
3840 Some(item::Dedup::ReplaceExisting) => {
3841 item_to_remove = Some((ix, item.item_id()));
3842 break;
3843 }
3844 None => {}
3845 }
3846 }
3847 }
3848
3849 if let Some((ix, id)) = item_to_remove {
3850 pane.remove_item(id, false, false, cx);
3851 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3852 }
3853 })?;
3854
3855 new_item
3856 };
3857
3858 this.update(cx, |this, cx| {
3859 let state = this.follower_states.get_mut(&leader_id)?;
3860 item.set_leader_peer_id(Some(leader_id), cx);
3861 state.items_by_leader_view_id.insert(
3862 id,
3863 FollowerView {
3864 view: item,
3865 location: panel_id,
3866 },
3867 );
3868
3869 Some(())
3870 })?;
3871
3872 Ok(())
3873 }
3874
3875 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3876 let mut is_project_item = true;
3877 let mut update = proto::UpdateActiveView::default();
3878 if cx.is_window_active() {
3879 let (active_item, panel_id) = self.active_item_for_followers(cx);
3880
3881 if let Some(item) = active_item {
3882 if item.focus_handle(cx).contains_focused(cx) {
3883 let leader_id = self
3884 .pane_for(&*item)
3885 .and_then(|pane| self.leader_for_pane(&pane));
3886
3887 if let Some(item) = item.to_followable_item_handle(cx) {
3888 let id = item
3889 .remote_id(&self.app_state.client, cx)
3890 .map(|id| id.to_proto());
3891
3892 if let Some(id) = id.clone() {
3893 if let Some(variant) = item.to_state_proto(cx) {
3894 let view = Some(proto::View {
3895 id: Some(id.clone()),
3896 leader_id,
3897 variant: Some(variant),
3898 panel_id: panel_id.map(|id| id as i32),
3899 });
3900
3901 is_project_item = item.is_project_item(cx);
3902 update = proto::UpdateActiveView {
3903 view,
3904 // TODO: Remove after version 0.145.x stabilizes.
3905 id: Some(id.clone()),
3906 leader_id,
3907 };
3908 }
3909 };
3910 }
3911 }
3912 }
3913 }
3914
3915 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3916 if active_view_id != self.last_active_view_id.as_ref() {
3917 self.last_active_view_id = active_view_id.cloned();
3918 self.update_followers(
3919 is_project_item,
3920 proto::update_followers::Variant::UpdateActiveView(update),
3921 cx,
3922 );
3923 }
3924 }
3925
3926 fn active_item_for_followers(
3927 &self,
3928 cx: &mut WindowContext,
3929 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3930 let mut active_item = None;
3931 let mut panel_id = None;
3932 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3933 if dock.focus_handle(cx).contains_focused(cx) {
3934 if let Some(panel) = dock.read(cx).active_panel() {
3935 if let Some(pane) = panel.pane(cx) {
3936 if let Some(item) = pane.read(cx).active_item() {
3937 active_item = Some(item);
3938 panel_id = panel.remote_id();
3939 break;
3940 }
3941 }
3942 }
3943 }
3944 }
3945
3946 if active_item.is_none() {
3947 active_item = self.active_pane().read(cx).active_item();
3948 }
3949 (active_item, panel_id)
3950 }
3951
3952 fn update_followers(
3953 &self,
3954 project_only: bool,
3955 update: proto::update_followers::Variant,
3956 cx: &mut WindowContext,
3957 ) -> Option<()> {
3958 // If this update only applies to for followers in the current project,
3959 // then skip it unless this project is shared. If it applies to all
3960 // followers, regardless of project, then set `project_id` to none,
3961 // indicating that it goes to all followers.
3962 let project_id = if project_only {
3963 Some(self.project.read(cx).remote_id()?)
3964 } else {
3965 None
3966 };
3967 self.app_state().workspace_store.update(cx, |store, cx| {
3968 store.update_followers(project_id, update, cx)
3969 })
3970 }
3971
3972 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3973 self.follower_states.iter().find_map(|(leader_id, state)| {
3974 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3975 Some(*leader_id)
3976 } else {
3977 None
3978 }
3979 })
3980 }
3981
3982 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3983 cx.notify();
3984
3985 let call = self.active_call()?;
3986 let room = call.read(cx).room()?.read(cx);
3987 let participant = room.remote_participant_for_peer_id(leader_id)?;
3988
3989 let leader_in_this_app;
3990 let leader_in_this_project;
3991 match participant.location {
3992 call::ParticipantLocation::SharedProject { project_id } => {
3993 leader_in_this_app = true;
3994 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3995 }
3996 call::ParticipantLocation::UnsharedProject => {
3997 leader_in_this_app = true;
3998 leader_in_this_project = false;
3999 }
4000 call::ParticipantLocation::External => {
4001 leader_in_this_app = false;
4002 leader_in_this_project = false;
4003 }
4004 };
4005
4006 let state = self.follower_states.get(&leader_id)?;
4007 let mut item_to_activate = None;
4008 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4009 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4010 if leader_in_this_project || !item.view.is_project_item(cx) {
4011 item_to_activate = Some((item.location, item.view.boxed_clone()));
4012 }
4013 }
4014 } else if let Some(shared_screen) =
4015 self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
4016 {
4017 item_to_activate = Some((None, Box::new(shared_screen)));
4018 }
4019
4020 let (panel_id, item) = item_to_activate?;
4021
4022 let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
4023 let pane;
4024 if let Some(panel_id) = panel_id {
4025 pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
4026 let state = self.follower_states.get_mut(&leader_id)?;
4027 state.dock_pane = Some(pane.clone());
4028 } else {
4029 pane = state.center_pane.clone();
4030 let state = self.follower_states.get_mut(&leader_id)?;
4031 if let Some(dock_pane) = state.dock_pane.take() {
4032 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
4033 }
4034 }
4035
4036 pane.update(cx, |pane, cx| {
4037 let focus_active_item = pane.has_focus(cx) || transfer_focus;
4038 if let Some(index) = pane.index_for_item(item.as_ref()) {
4039 pane.activate_item(index, false, false, cx);
4040 } else {
4041 pane.add_item(item.boxed_clone(), false, false, None, cx)
4042 }
4043
4044 if focus_active_item {
4045 pane.focus_active_item(cx)
4046 }
4047 });
4048
4049 None
4050 }
4051
4052 #[cfg(target_os = "windows")]
4053 fn shared_screen_for_peer(
4054 &self,
4055 _peer_id: PeerId,
4056 _pane: &View<Pane>,
4057 _cx: &mut WindowContext,
4058 ) -> Option<View<SharedScreen>> {
4059 None
4060 }
4061
4062 #[cfg(not(target_os = "windows"))]
4063 fn shared_screen_for_peer(
4064 &self,
4065 peer_id: PeerId,
4066 pane: &View<Pane>,
4067 cx: &mut WindowContext,
4068 ) -> Option<View<SharedScreen>> {
4069 let call = self.active_call()?;
4070 let room = call.read(cx).room()?.read(cx);
4071 let participant = room.remote_participant_for_peer_id(peer_id)?;
4072 let track = participant.video_tracks.values().next()?.clone();
4073 let user = participant.user.clone();
4074
4075 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4076 if item.read(cx).peer_id == peer_id {
4077 return Some(item);
4078 }
4079 }
4080
4081 Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx)))
4082 }
4083
4084 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
4085 if cx.is_window_active() {
4086 self.update_active_view_for_followers(cx);
4087
4088 if let Some(database_id) = self.database_id {
4089 cx.background_executor()
4090 .spawn(persistence::DB.update_timestamp(database_id))
4091 .detach();
4092 }
4093 } else {
4094 for pane in &self.panes {
4095 pane.update(cx, |pane, cx| {
4096 if let Some(item) = pane.active_item() {
4097 item.workspace_deactivated(cx);
4098 }
4099 for item in pane.items() {
4100 if matches!(
4101 item.workspace_settings(cx).autosave,
4102 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4103 ) {
4104 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
4105 .detach_and_log_err(cx);
4106 }
4107 }
4108 });
4109 }
4110 }
4111 }
4112
4113 pub fn active_call(&self) -> Option<&Model<ActiveCall>> {
4114 self.active_call.as_ref().map(|(call, _)| call)
4115 }
4116
4117 fn on_active_call_event(
4118 &mut self,
4119 _: Model<ActiveCall>,
4120 event: &call::room::Event,
4121 cx: &mut ViewContext<Self>,
4122 ) {
4123 match event {
4124 call::room::Event::ParticipantLocationChanged { participant_id }
4125 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4126 self.leader_updated(*participant_id, cx);
4127 }
4128 _ => {}
4129 }
4130 }
4131
4132 pub fn database_id(&self) -> Option<WorkspaceId> {
4133 self.database_id
4134 }
4135
4136 fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
4137 let project = self.project().read(cx);
4138
4139 if project.is_local() {
4140 Some(
4141 project
4142 .visible_worktrees(cx)
4143 .map(|worktree| worktree.read(cx).abs_path())
4144 .collect::<Vec<_>>(),
4145 )
4146 } else {
4147 None
4148 }
4149 }
4150
4151 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
4152 match member {
4153 Member::Axis(PaneAxis { members, .. }) => {
4154 for child in members.iter() {
4155 self.remove_panes(child.clone(), cx)
4156 }
4157 }
4158 Member::Pane(pane) => {
4159 self.force_remove_pane(&pane, &None, cx);
4160 }
4161 }
4162 }
4163
4164 fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
4165 self.session_id.take();
4166 self.serialize_workspace_internal(cx)
4167 }
4168
4169 fn force_remove_pane(
4170 &mut self,
4171 pane: &View<Pane>,
4172 focus_on: &Option<View<Pane>>,
4173 cx: &mut ViewContext<Workspace>,
4174 ) {
4175 self.panes.retain(|p| p != pane);
4176 if let Some(focus_on) = focus_on {
4177 focus_on.update(cx, |pane, cx| pane.focus(cx));
4178 } else {
4179 self.panes
4180 .last()
4181 .unwrap()
4182 .update(cx, |pane, cx| pane.focus(cx));
4183 }
4184 if self.last_active_center_pane == Some(pane.downgrade()) {
4185 self.last_active_center_pane = None;
4186 }
4187 cx.notify();
4188 }
4189
4190 fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4191 if self._schedule_serialize.is_none() {
4192 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4193 cx.background_executor()
4194 .timer(Duration::from_millis(100))
4195 .await;
4196 this.update(&mut cx, |this, cx| {
4197 this.serialize_workspace_internal(cx).detach();
4198 this._schedule_serialize.take();
4199 })
4200 .log_err();
4201 }));
4202 }
4203 }
4204
4205 fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4206 let Some(database_id) = self.database_id() else {
4207 return Task::ready(());
4208 };
4209
4210 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4211 let (items, active, pinned_count) = {
4212 let pane = pane_handle.read(cx);
4213 let active_item_id = pane.active_item().map(|item| item.item_id());
4214 (
4215 pane.items()
4216 .filter_map(|handle| {
4217 let handle = handle.to_serializable_item_handle(cx)?;
4218
4219 Some(SerializedItem {
4220 kind: Arc::from(handle.serialized_item_kind()),
4221 item_id: handle.item_id().as_u64(),
4222 active: Some(handle.item_id()) == active_item_id,
4223 preview: pane.is_active_preview_item(handle.item_id()),
4224 })
4225 })
4226 .collect::<Vec<_>>(),
4227 pane.has_focus(cx),
4228 pane.pinned_count(),
4229 )
4230 };
4231
4232 SerializedPane::new(items, active, pinned_count)
4233 }
4234
4235 fn build_serialized_pane_group(
4236 pane_group: &Member,
4237 cx: &WindowContext,
4238 ) -> SerializedPaneGroup {
4239 match pane_group {
4240 Member::Axis(PaneAxis {
4241 axis,
4242 members,
4243 flexes,
4244 bounding_boxes: _,
4245 }) => SerializedPaneGroup::Group {
4246 axis: SerializedAxis(*axis),
4247 children: members
4248 .iter()
4249 .map(|member| build_serialized_pane_group(member, cx))
4250 .collect::<Vec<_>>(),
4251 flexes: Some(flexes.lock().clone()),
4252 },
4253 Member::Pane(pane_handle) => {
4254 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4255 }
4256 }
4257 }
4258
4259 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4260 let left_dock = this.left_dock.read(cx);
4261 let left_visible = left_dock.is_open();
4262 let left_active_panel = left_dock
4263 .active_panel()
4264 .map(|panel| panel.persistent_name().to_string());
4265 let left_dock_zoom = left_dock
4266 .active_panel()
4267 .map(|panel| panel.is_zoomed(cx))
4268 .unwrap_or(false);
4269
4270 let right_dock = this.right_dock.read(cx);
4271 let right_visible = right_dock.is_open();
4272 let right_active_panel = right_dock
4273 .active_panel()
4274 .map(|panel| panel.persistent_name().to_string());
4275 let right_dock_zoom = right_dock
4276 .active_panel()
4277 .map(|panel| panel.is_zoomed(cx))
4278 .unwrap_or(false);
4279
4280 let bottom_dock = this.bottom_dock.read(cx);
4281 let bottom_visible = bottom_dock.is_open();
4282 let bottom_active_panel = bottom_dock
4283 .active_panel()
4284 .map(|panel| panel.persistent_name().to_string());
4285 let bottom_dock_zoom = bottom_dock
4286 .active_panel()
4287 .map(|panel| panel.is_zoomed(cx))
4288 .unwrap_or(false);
4289
4290 DockStructure {
4291 left: DockData {
4292 visible: left_visible,
4293 active_panel: left_active_panel,
4294 zoom: left_dock_zoom,
4295 },
4296 right: DockData {
4297 visible: right_visible,
4298 active_panel: right_active_panel,
4299 zoom: right_dock_zoom,
4300 },
4301 bottom: DockData {
4302 visible: bottom_visible,
4303 active_panel: bottom_active_panel,
4304 zoom: bottom_dock_zoom,
4305 },
4306 }
4307 }
4308
4309 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4310 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4311 } else if let Some(local_paths) = self.local_paths(cx) {
4312 if !local_paths.is_empty() {
4313 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4314 } else {
4315 None
4316 }
4317 } else {
4318 None
4319 };
4320
4321 if let Some(location) = location {
4322 let center_group = build_serialized_pane_group(&self.center.root, cx);
4323 let docks = build_serialized_docks(self, cx);
4324 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4325 let serialized_workspace = SerializedWorkspace {
4326 id: database_id,
4327 location,
4328 center_group,
4329 window_bounds,
4330 display: Default::default(),
4331 docks,
4332 centered_layout: self.centered_layout,
4333 session_id: self.session_id.clone(),
4334 window_id: Some(cx.window_handle().window_id().as_u64()),
4335 };
4336 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4337 }
4338 Task::ready(())
4339 }
4340
4341 async fn serialize_items(
4342 this: &WeakView<Self>,
4343 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4344 cx: &mut AsyncWindowContext,
4345 ) -> Result<()> {
4346 const CHUNK_SIZE: usize = 200;
4347 const THROTTLE_TIME: Duration = Duration::from_millis(200);
4348
4349 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4350
4351 while let Some(items_received) = serializable_items.next().await {
4352 let unique_items =
4353 items_received
4354 .into_iter()
4355 .fold(HashMap::default(), |mut acc, item| {
4356 acc.entry(item.item_id()).or_insert(item);
4357 acc
4358 });
4359
4360 // We use into_iter() here so that the references to the items are moved into
4361 // the tasks and not kept alive while we're sleeping.
4362 for (_, item) in unique_items.into_iter() {
4363 if let Ok(Some(task)) =
4364 this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4365 {
4366 cx.background_executor()
4367 .spawn(async move { task.await.log_err() })
4368 .detach();
4369 }
4370 }
4371
4372 cx.background_executor().timer(THROTTLE_TIME).await;
4373 }
4374
4375 Ok(())
4376 }
4377
4378 pub(crate) fn enqueue_item_serialization(
4379 &mut self,
4380 item: Box<dyn SerializableItemHandle>,
4381 ) -> Result<()> {
4382 self.serializable_items_tx
4383 .unbounded_send(item)
4384 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4385 }
4386
4387 pub(crate) fn load_workspace(
4388 serialized_workspace: SerializedWorkspace,
4389 paths_to_open: Vec<Option<ProjectPath>>,
4390 cx: &mut ViewContext<Workspace>,
4391 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4392 cx.spawn(|workspace, mut cx| async move {
4393 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4394
4395 let mut center_group = None;
4396 let mut center_items = None;
4397
4398 // Traverse the splits tree and add to things
4399 if let Some((group, active_pane, items)) = serialized_workspace
4400 .center_group
4401 .deserialize(
4402 &project,
4403 serialized_workspace.id,
4404 workspace.clone(),
4405 &mut cx,
4406 )
4407 .await
4408 {
4409 center_items = Some(items);
4410 center_group = Some((group, active_pane))
4411 }
4412
4413 let mut items_by_project_path = HashMap::default();
4414 let mut item_ids_by_kind = HashMap::default();
4415 let mut all_deserialized_items = Vec::default();
4416 cx.update(|cx| {
4417 for item in center_items.unwrap_or_default().into_iter().flatten() {
4418 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4419 item_ids_by_kind
4420 .entry(serializable_item_handle.serialized_item_kind())
4421 .or_insert(Vec::new())
4422 .push(item.item_id().as_u64() as ItemId);
4423 }
4424
4425 if let Some(project_path) = item.project_path(cx) {
4426 items_by_project_path.insert(project_path, item.clone());
4427 }
4428 all_deserialized_items.push(item);
4429 }
4430 })?;
4431
4432 let opened_items = paths_to_open
4433 .into_iter()
4434 .map(|path_to_open| {
4435 path_to_open
4436 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4437 })
4438 .collect::<Vec<_>>();
4439
4440 // Remove old panes from workspace panes list
4441 workspace.update(&mut cx, |workspace, cx| {
4442 if let Some((center_group, active_pane)) = center_group {
4443 workspace.remove_panes(workspace.center.root.clone(), cx);
4444
4445 // Swap workspace center group
4446 workspace.center = PaneGroup::with_root(center_group);
4447 if let Some(active_pane) = active_pane {
4448 workspace.set_active_pane(&active_pane, cx);
4449 cx.focus_self();
4450 } else {
4451 workspace.set_active_pane(&workspace.center.first_pane(), cx);
4452 }
4453 }
4454
4455 let docks = serialized_workspace.docks;
4456
4457 for (dock, serialized_dock) in [
4458 (&mut workspace.right_dock, docks.right),
4459 (&mut workspace.left_dock, docks.left),
4460 (&mut workspace.bottom_dock, docks.bottom),
4461 ]
4462 .iter_mut()
4463 {
4464 dock.update(cx, |dock, cx| {
4465 dock.serialized_dock = Some(serialized_dock.clone());
4466 dock.restore_state(cx);
4467 });
4468 }
4469
4470 cx.notify();
4471 })?;
4472
4473 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4474 // after loading the items, we might have different items and in order to avoid
4475 // the database filling up, we delete items that haven't been loaded now.
4476 //
4477 // The items that have been loaded, have been saved after they've been added to the workspace.
4478 let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4479 item_ids_by_kind
4480 .into_iter()
4481 .map(|(item_kind, loaded_items)| {
4482 SerializableItemRegistry::cleanup(
4483 item_kind,
4484 serialized_workspace.id,
4485 loaded_items,
4486 cx,
4487 )
4488 .log_err()
4489 })
4490 .collect::<Vec<_>>()
4491 })?;
4492
4493 futures::future::join_all(clean_up_tasks).await;
4494
4495 workspace
4496 .update(&mut cx, |workspace, cx| {
4497 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4498 workspace.serialize_workspace_internal(cx).detach();
4499
4500 // Ensure that we mark the window as edited if we did load dirty items
4501 workspace.update_window_edited(cx);
4502 })
4503 .ok();
4504
4505 Ok(opened_items)
4506 })
4507 }
4508
4509 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4510 self.add_workspace_actions_listeners(div, cx)
4511 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4512 .on_action(cx.listener(Self::close_all_items_and_panes))
4513 .on_action(cx.listener(Self::save_all))
4514 .on_action(cx.listener(Self::send_keystrokes))
4515 .on_action(cx.listener(Self::add_folder_to_project))
4516 .on_action(cx.listener(Self::follow_next_collaborator))
4517 .on_action(cx.listener(Self::close_window))
4518 .on_action(cx.listener(Self::activate_pane_at_index))
4519 .on_action(cx.listener(Self::move_item_to_pane_at_index))
4520 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4521 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4522 let pane = workspace.active_pane().clone();
4523 workspace.unfollow_in_pane(&pane, cx);
4524 }))
4525 .on_action(cx.listener(|workspace, action: &Save, cx| {
4526 workspace
4527 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4528 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4529 }))
4530 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4531 workspace
4532 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4533 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4534 }))
4535 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4536 workspace
4537 .save_active_item(SaveIntent::SaveAs, cx)
4538 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4539 }))
4540 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4541 workspace.activate_previous_pane(cx)
4542 }))
4543 .on_action(
4544 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4545 )
4546 .on_action(cx.listener(|workspace, _: &ActivateNextWindow, cx| {
4547 workspace.activate_next_window(cx)
4548 }))
4549 .on_action(cx.listener(|workspace, _: &ActivatePreviousWindow, cx| {
4550 workspace.activate_previous_window(cx)
4551 }))
4552 .on_action(
4553 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4554 workspace.activate_pane_in_direction(action.0, cx)
4555 }),
4556 )
4557 .on_action(
4558 cx.listener(|workspace, action: &MoveItemToPaneInDirection, cx| {
4559 workspace.move_item_to_pane_in_direction(action, cx)
4560 }),
4561 )
4562 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4563 workspace.swap_pane_in_direction(action.0, cx)
4564 }))
4565 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4566 this.toggle_dock(DockPosition::Left, cx);
4567 }))
4568 .on_action(
4569 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4570 workspace.toggle_dock(DockPosition::Right, cx);
4571 }),
4572 )
4573 .on_action(
4574 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4575 workspace.toggle_dock(DockPosition::Bottom, cx);
4576 }),
4577 )
4578 .on_action(
4579 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4580 workspace.close_all_docks(cx);
4581 }),
4582 )
4583 .on_action(
4584 cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4585 workspace.clear_all_notifications(cx);
4586 }),
4587 )
4588 .on_action(
4589 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4590 workspace.reopen_closed_item(cx).detach();
4591 }),
4592 )
4593 .on_action(cx.listener(Workspace::toggle_centered_layout))
4594 }
4595
4596 #[cfg(any(test, feature = "test-support"))]
4597 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4598 use node_runtime::NodeRuntime;
4599 use session::Session;
4600
4601 let client = project.read(cx).client();
4602 let user_store = project.read(cx).user_store();
4603
4604 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4605 let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4606 cx.activate_window();
4607 let app_state = Arc::new(AppState {
4608 languages: project.read(cx).languages().clone(),
4609 workspace_store,
4610 client,
4611 user_store,
4612 fs: project.read(cx).fs().clone(),
4613 build_window_options: |_, _| Default::default(),
4614 node_runtime: NodeRuntime::unavailable(),
4615 session,
4616 });
4617 let workspace = Self::new(Default::default(), project, app_state, cx);
4618 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4619 workspace
4620 }
4621
4622 pub fn register_action<A: Action>(
4623 &mut self,
4624 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4625 ) -> &mut Self {
4626 let callback = Arc::new(callback);
4627
4628 self.workspace_actions.push(Box::new(move |div, cx| {
4629 let callback = callback.clone();
4630 div.on_action(
4631 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4632 )
4633 }));
4634 self
4635 }
4636
4637 fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4638 for action in self.workspace_actions.iter() {
4639 div = (action)(div, cx)
4640 }
4641 div
4642 }
4643
4644 pub fn has_active_modal(&self, cx: &WindowContext) -> bool {
4645 self.modal_layer.read(cx).has_active_modal()
4646 }
4647
4648 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
4649 self.modal_layer.read(cx).active_modal()
4650 }
4651
4652 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4653 where
4654 B: FnOnce(&mut ViewContext<V>) -> V,
4655 {
4656 self.modal_layer
4657 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4658 }
4659
4660 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4661 self.centered_layout = !self.centered_layout;
4662 if let Some(database_id) = self.database_id() {
4663 cx.background_executor()
4664 .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4665 .detach_and_log_err(cx);
4666 }
4667 cx.notify();
4668 }
4669
4670 fn adjust_padding(padding: Option<f32>) -> f32 {
4671 padding
4672 .unwrap_or(Self::DEFAULT_PADDING)
4673 .clamp(0.0, Self::MAX_PADDING)
4674 }
4675
4676 fn render_dock(
4677 &self,
4678 position: DockPosition,
4679 dock: &View<Dock>,
4680 cx: &WindowContext,
4681 ) -> Option<Div> {
4682 if self.zoomed_position == Some(position) {
4683 return None;
4684 }
4685
4686 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4687 let pane = panel.pane(cx)?;
4688 let follower_states = &self.follower_states;
4689 leader_border_for_pane(follower_states, &pane, cx)
4690 });
4691
4692 Some(
4693 div()
4694 .flex()
4695 .flex_none()
4696 .overflow_hidden()
4697 .child(dock.clone())
4698 .children(leader_border),
4699 )
4700 }
4701
4702 pub fn for_window(cx: &mut WindowContext) -> Option<View<Workspace>> {
4703 let window = cx.window_handle().downcast::<Workspace>()?;
4704 cx.read_window(&window, |workspace, _| workspace).ok()
4705 }
4706
4707 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
4708 self.zoomed.as_ref()
4709 }
4710
4711 pub fn activate_next_window(&mut self, cx: &mut ViewContext<Self>) {
4712 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
4713 return;
4714 };
4715 let windows = cx.windows();
4716 let Some(next_window) = windows
4717 .iter()
4718 .cycle()
4719 .skip_while(|window| window.window_id() != current_window_id)
4720 .nth(1)
4721 else {
4722 return;
4723 };
4724 next_window.update(cx, |_, cx| cx.activate_window()).ok();
4725 }
4726
4727 pub fn activate_previous_window(&mut self, cx: &mut ViewContext<Self>) {
4728 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
4729 return;
4730 };
4731 let windows = cx.windows();
4732 let Some(prev_window) = windows
4733 .iter()
4734 .rev()
4735 .cycle()
4736 .skip_while(|window| window.window_id() != current_window_id)
4737 .nth(1)
4738 else {
4739 return;
4740 };
4741 prev_window.update(cx, |_, cx| cx.activate_window()).ok();
4742 }
4743}
4744
4745fn leader_border_for_pane(
4746 follower_states: &HashMap<PeerId, FollowerState>,
4747 pane: &View<Pane>,
4748 cx: &WindowContext,
4749) -> Option<Div> {
4750 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4751 if state.pane() == pane {
4752 Some((*leader_id, state))
4753 } else {
4754 None
4755 }
4756 })?;
4757
4758 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4759 let leader = room.remote_participant_for_peer_id(leader_id)?;
4760
4761 let mut leader_color = cx
4762 .theme()
4763 .players()
4764 .color_for_participant(leader.participant_index.0)
4765 .cursor;
4766 leader_color.fade_out(0.3);
4767 Some(
4768 div()
4769 .absolute()
4770 .size_full()
4771 .left_0()
4772 .top_0()
4773 .border_2()
4774 .border_color(leader_color),
4775 )
4776}
4777
4778fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4779 ZED_WINDOW_POSITION
4780 .zip(*ZED_WINDOW_SIZE)
4781 .map(|(position, size)| Bounds {
4782 origin: position,
4783 size,
4784 })
4785}
4786
4787fn open_items(
4788 serialized_workspace: Option<SerializedWorkspace>,
4789 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4790 cx: &mut ViewContext<Workspace>,
4791) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4792 let restored_items = serialized_workspace.map(|serialized_workspace| {
4793 Workspace::load_workspace(
4794 serialized_workspace,
4795 project_paths_to_open
4796 .iter()
4797 .map(|(_, project_path)| project_path)
4798 .cloned()
4799 .collect(),
4800 cx,
4801 )
4802 });
4803
4804 cx.spawn(|workspace, mut cx| async move {
4805 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4806
4807 if let Some(restored_items) = restored_items {
4808 let restored_items = restored_items.await?;
4809
4810 let restored_project_paths = restored_items
4811 .iter()
4812 .filter_map(|item| {
4813 cx.update(|cx| item.as_ref()?.project_path(cx))
4814 .ok()
4815 .flatten()
4816 })
4817 .collect::<HashSet<_>>();
4818
4819 for restored_item in restored_items {
4820 opened_items.push(restored_item.map(Ok));
4821 }
4822
4823 project_paths_to_open
4824 .iter_mut()
4825 .for_each(|(_, project_path)| {
4826 if let Some(project_path_to_open) = project_path {
4827 if restored_project_paths.contains(project_path_to_open) {
4828 *project_path = None;
4829 }
4830 }
4831 });
4832 } else {
4833 for _ in 0..project_paths_to_open.len() {
4834 opened_items.push(None);
4835 }
4836 }
4837 assert!(opened_items.len() == project_paths_to_open.len());
4838
4839 let tasks =
4840 project_paths_to_open
4841 .into_iter()
4842 .enumerate()
4843 .map(|(ix, (abs_path, project_path))| {
4844 let workspace = workspace.clone();
4845 cx.spawn(|mut cx| async move {
4846 let file_project_path = project_path?;
4847 let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4848 workspace.project().update(cx, |project, cx| {
4849 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4850 })
4851 });
4852
4853 // We only want to open file paths here. If one of the items
4854 // here is a directory, it was already opened further above
4855 // with a `find_or_create_worktree`.
4856 if let Ok(task) = abs_path_task {
4857 if task.await.map_or(true, |p| p.is_file()) {
4858 return Some((
4859 ix,
4860 workspace
4861 .update(&mut cx, |workspace, cx| {
4862 workspace.open_path(file_project_path, None, true, cx)
4863 })
4864 .log_err()?
4865 .await,
4866 ));
4867 }
4868 }
4869 None
4870 })
4871 });
4872
4873 let tasks = tasks.collect::<Vec<_>>();
4874
4875 let tasks = futures::future::join_all(tasks);
4876 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4877 opened_items[ix] = Some(path_open_result);
4878 }
4879
4880 Ok(opened_items)
4881 })
4882}
4883
4884enum ActivateInDirectionTarget {
4885 Pane(View<Pane>),
4886 Dock(View<Dock>),
4887}
4888
4889fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4890 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=admin+read%2Ctriage%2Cbug&projects=&template=1_bug_report.yml";
4891
4892 workspace
4893 .update(cx, |workspace, cx| {
4894 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4895 struct DatabaseFailedNotification;
4896
4897 workspace.show_notification(
4898 NotificationId::unique::<DatabaseFailedNotification>(),
4899 cx,
4900 |cx| {
4901 cx.new_view(|_| {
4902 MessageNotification::new("Failed to load the database file.")
4903 .with_click_message("File an issue")
4904 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4905 })
4906 },
4907 );
4908 }
4909 })
4910 .log_err();
4911}
4912
4913impl FocusableView for Workspace {
4914 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4915 self.active_pane.focus_handle(cx)
4916 }
4917}
4918
4919#[derive(Clone, Render)]
4920struct DraggedDock(DockPosition);
4921
4922impl Render for Workspace {
4923 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4924 let mut context = KeyContext::new_with_defaults();
4925 context.add("Workspace");
4926 context.set("keyboard_layout", cx.keyboard_layout().clone());
4927 let centered_layout = self.centered_layout
4928 && self.center.panes().len() == 1
4929 && self.active_item(cx).is_some();
4930 let render_padding = |size| {
4931 (size > 0.0).then(|| {
4932 div()
4933 .h_full()
4934 .w(relative(size))
4935 .bg(cx.theme().colors().editor_background)
4936 .border_color(cx.theme().colors().pane_group_border)
4937 })
4938 };
4939 let paddings = if centered_layout {
4940 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4941 (
4942 render_padding(Self::adjust_padding(settings.left_padding)),
4943 render_padding(Self::adjust_padding(settings.right_padding)),
4944 )
4945 } else {
4946 (None, None)
4947 };
4948 let ui_font = theme::setup_ui_font(cx);
4949
4950 let theme = cx.theme().clone();
4951 let colors = theme.colors();
4952
4953 client_side_decorations(
4954 self.actions(div(), cx)
4955 .key_context(context)
4956 .relative()
4957 .size_full()
4958 .flex()
4959 .flex_col()
4960 .font(ui_font)
4961 .gap_0()
4962 .justify_start()
4963 .items_start()
4964 .text_color(colors.text)
4965 .overflow_hidden()
4966 .children(self.titlebar_item.clone())
4967 .child(
4968 div()
4969 .size_full()
4970 .relative()
4971 .flex_1()
4972 .flex()
4973 .flex_col()
4974 .child(
4975 div()
4976 .id("workspace")
4977 .bg(colors.background)
4978 .relative()
4979 .flex_1()
4980 .w_full()
4981 .flex()
4982 .flex_col()
4983 .overflow_hidden()
4984 .border_t_1()
4985 .border_b_1()
4986 .border_color(colors.border)
4987 .child({
4988 let this = cx.view().clone();
4989 canvas(
4990 move |bounds, cx| {
4991 this.update(cx, |this, cx| {
4992 let bounds_changed = this.bounds != bounds;
4993 this.bounds = bounds;
4994
4995 if bounds_changed {
4996 this.left_dock.update(cx, |dock, cx| {
4997 dock.clamp_panel_size(bounds.size.width, cx)
4998 });
4999
5000 this.right_dock.update(cx, |dock, cx| {
5001 dock.clamp_panel_size(bounds.size.width, cx)
5002 });
5003
5004 this.bottom_dock.update(cx, |dock, cx| {
5005 dock.clamp_panel_size(
5006 bounds.size.height,
5007 cx,
5008 )
5009 });
5010 }
5011 })
5012 },
5013 |_, _, _| {},
5014 )
5015 .absolute()
5016 .size_full()
5017 })
5018 .when(self.zoomed.is_none(), |this| {
5019 this.on_drag_move(cx.listener(
5020 move |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
5021 if workspace.previous_dock_drag_coordinates
5022 != Some(e.event.position)
5023 {
5024 workspace.previous_dock_drag_coordinates =
5025 Some(e.event.position);
5026 match e.drag(cx).0 {
5027 DockPosition::Left => {
5028 resize_left_dock(
5029 e.event.position.x
5030 - workspace.bounds.left(),
5031 workspace,
5032 cx,
5033 );
5034 }
5035 DockPosition::Right => {
5036 resize_right_dock(
5037 workspace.bounds.right()
5038 - e.event.position.x,
5039 workspace,
5040 cx,
5041 );
5042 }
5043 DockPosition::Bottom => {
5044 resize_bottom_dock(
5045 workspace.bounds.bottom()
5046 - e.event.position.y,
5047 workspace,
5048 cx,
5049 );
5050 }
5051 };
5052 workspace.serialize_workspace(cx);
5053 }
5054 },
5055 ))
5056 })
5057 .child(
5058 div()
5059 .flex()
5060 .flex_row()
5061 .h_full()
5062 // Left Dock
5063 .children(self.render_dock(
5064 DockPosition::Left,
5065 &self.left_dock,
5066 cx,
5067 ))
5068 // Panes
5069 .child(
5070 div()
5071 .flex()
5072 .flex_col()
5073 .flex_1()
5074 .overflow_hidden()
5075 .child(
5076 h_flex()
5077 .flex_1()
5078 .when_some(paddings.0, |this, p| {
5079 this.child(p.border_r_1())
5080 })
5081 .child(self.center.render(
5082 &self.project,
5083 &self.follower_states,
5084 self.active_call(),
5085 &self.active_pane,
5086 self.zoomed.as_ref(),
5087 &self.app_state,
5088 cx,
5089 ))
5090 .when_some(paddings.1, |this, p| {
5091 this.child(p.border_l_1())
5092 }),
5093 )
5094 .children(self.render_dock(
5095 DockPosition::Bottom,
5096 &self.bottom_dock,
5097 cx,
5098 )),
5099 )
5100 // Right Dock
5101 .children(self.render_dock(
5102 DockPosition::Right,
5103 &self.right_dock,
5104 cx,
5105 )),
5106 )
5107 .children(self.zoomed.as_ref().and_then(|view| {
5108 let zoomed_view = view.upgrade()?;
5109 let div = div()
5110 .occlude()
5111 .absolute()
5112 .overflow_hidden()
5113 .border_color(colors.border)
5114 .bg(colors.background)
5115 .child(zoomed_view)
5116 .inset_0()
5117 .shadow_lg();
5118
5119 Some(match self.zoomed_position {
5120 Some(DockPosition::Left) => div.right_2().border_r_1(),
5121 Some(DockPosition::Right) => div.left_2().border_l_1(),
5122 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5123 None => {
5124 div.top_2().bottom_2().left_2().right_2().border_1()
5125 }
5126 })
5127 }))
5128 .children(self.render_notifications(cx)),
5129 )
5130 .child(self.status_bar.clone())
5131 .child(self.modal_layer.clone()),
5132 ),
5133 cx,
5134 )
5135 }
5136}
5137
5138fn resize_bottom_dock(
5139 new_size: Pixels,
5140 workspace: &mut Workspace,
5141 cx: &mut ViewContext<Workspace>,
5142) {
5143 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5144 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5145 bottom_dock.resize_active_panel(Some(size), cx);
5146 });
5147}
5148
5149fn resize_right_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5150 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5151 workspace.right_dock.update(cx, |right_dock, cx| {
5152 right_dock.resize_active_panel(Some(size), cx);
5153 });
5154}
5155
5156fn resize_left_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5157 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5158
5159 workspace.left_dock.update(cx, |left_dock, cx| {
5160 left_dock.resize_active_panel(Some(size), cx);
5161 });
5162}
5163
5164impl WorkspaceStore {
5165 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
5166 Self {
5167 workspaces: Default::default(),
5168 _subscriptions: vec![
5169 client.add_request_handler(cx.weak_model(), Self::handle_follow),
5170 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
5171 ],
5172 client,
5173 }
5174 }
5175
5176 pub fn update_followers(
5177 &self,
5178 project_id: Option<u64>,
5179 update: proto::update_followers::Variant,
5180 cx: &AppContext,
5181 ) -> Option<()> {
5182 let active_call = ActiveCall::try_global(cx)?;
5183 let room_id = active_call.read(cx).room()?.read(cx).id();
5184 self.client
5185 .send(proto::UpdateFollowers {
5186 room_id,
5187 project_id,
5188 variant: Some(update),
5189 })
5190 .log_err()
5191 }
5192
5193 pub async fn handle_follow(
5194 this: Model<Self>,
5195 envelope: TypedEnvelope<proto::Follow>,
5196 mut cx: AsyncAppContext,
5197 ) -> Result<proto::FollowResponse> {
5198 this.update(&mut cx, |this, cx| {
5199 let follower = Follower {
5200 project_id: envelope.payload.project_id,
5201 peer_id: envelope.original_sender_id()?,
5202 };
5203
5204 let mut response = proto::FollowResponse::default();
5205 this.workspaces.retain(|workspace| {
5206 workspace
5207 .update(cx, |workspace, cx| {
5208 let handler_response = workspace.handle_follow(follower.project_id, cx);
5209 if let Some(active_view) = handler_response.active_view.clone() {
5210 if workspace.project.read(cx).remote_id() == follower.project_id {
5211 response.active_view = Some(active_view)
5212 }
5213 }
5214 })
5215 .is_ok()
5216 });
5217
5218 Ok(response)
5219 })?
5220 }
5221
5222 async fn handle_update_followers(
5223 this: Model<Self>,
5224 envelope: TypedEnvelope<proto::UpdateFollowers>,
5225 mut cx: AsyncAppContext,
5226 ) -> Result<()> {
5227 let leader_id = envelope.original_sender_id()?;
5228 let update = envelope.payload;
5229
5230 this.update(&mut cx, |this, cx| {
5231 this.workspaces.retain(|workspace| {
5232 workspace
5233 .update(cx, |workspace, cx| {
5234 let project_id = workspace.project.read(cx).remote_id();
5235 if update.project_id != project_id && update.project_id.is_some() {
5236 return;
5237 }
5238 workspace.handle_update_followers(leader_id, update.clone(), cx);
5239 })
5240 .is_ok()
5241 });
5242 Ok(())
5243 })?
5244 }
5245}
5246
5247impl ViewId {
5248 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5249 Ok(Self {
5250 creator: message
5251 .creator
5252 .ok_or_else(|| anyhow!("creator is missing"))?,
5253 id: message.id,
5254 })
5255 }
5256
5257 pub(crate) fn to_proto(self) -> proto::ViewId {
5258 proto::ViewId {
5259 creator: Some(self.creator),
5260 id: self.id,
5261 }
5262 }
5263}
5264
5265impl FollowerState {
5266 fn pane(&self) -> &View<Pane> {
5267 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5268 }
5269}
5270
5271pub trait WorkspaceHandle {
5272 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5273}
5274
5275impl WorkspaceHandle for View<Workspace> {
5276 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5277 self.read(cx)
5278 .worktrees(cx)
5279 .flat_map(|worktree| {
5280 let worktree_id = worktree.read(cx).id();
5281 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5282 worktree_id,
5283 path: f.path.clone(),
5284 })
5285 })
5286 .collect::<Vec<_>>()
5287 }
5288}
5289
5290impl std::fmt::Debug for OpenPaths {
5291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5292 f.debug_struct("OpenPaths")
5293 .field("paths", &self.paths)
5294 .finish()
5295 }
5296}
5297
5298pub fn activate_workspace_for_project(
5299 cx: &mut AppContext,
5300 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5301) -> Option<WindowHandle<Workspace>> {
5302 for window in cx.windows() {
5303 let Some(workspace) = window.downcast::<Workspace>() else {
5304 continue;
5305 };
5306
5307 let predicate = workspace
5308 .update(cx, |workspace, cx| {
5309 let project = workspace.project.read(cx);
5310 if predicate(project, cx) {
5311 cx.activate_window();
5312 true
5313 } else {
5314 false
5315 }
5316 })
5317 .log_err()
5318 .unwrap_or(false);
5319
5320 if predicate {
5321 return Some(workspace);
5322 }
5323 }
5324
5325 None
5326}
5327
5328pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5329 DB.last_workspace().await.log_err().flatten()
5330}
5331
5332pub fn last_session_workspace_locations(
5333 last_session_id: &str,
5334 last_session_window_stack: Option<Vec<WindowId>>,
5335) -> Option<Vec<SerializedWorkspaceLocation>> {
5336 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5337 .log_err()
5338}
5339
5340actions!(collab, [OpenChannelNotes]);
5341actions!(zed, [OpenLog]);
5342
5343async fn join_channel_internal(
5344 channel_id: ChannelId,
5345 app_state: &Arc<AppState>,
5346 requesting_window: Option<WindowHandle<Workspace>>,
5347 active_call: &Model<ActiveCall>,
5348 cx: &mut AsyncAppContext,
5349) -> Result<bool> {
5350 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5351 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5352 return (false, None);
5353 };
5354
5355 let already_in_channel = room.channel_id() == Some(channel_id);
5356 let should_prompt = room.is_sharing_project()
5357 && !room.remote_participants().is_empty()
5358 && !already_in_channel;
5359 let open_room = if already_in_channel {
5360 active_call.room().cloned()
5361 } else {
5362 None
5363 };
5364 (should_prompt, open_room)
5365 })?;
5366
5367 if let Some(room) = open_room {
5368 let task = room.update(cx, |room, cx| {
5369 if let Some((project, host)) = room.most_active_project(cx) {
5370 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5371 }
5372
5373 None
5374 })?;
5375 if let Some(task) = task {
5376 task.await?;
5377 }
5378 return anyhow::Ok(true);
5379 }
5380
5381 if should_prompt {
5382 if let Some(workspace) = requesting_window {
5383 let answer = workspace
5384 .update(cx, |_, cx| {
5385 cx.prompt(
5386 PromptLevel::Warning,
5387 "Do you want to switch channels?",
5388 Some("Leaving this call will unshare your current project."),
5389 &["Yes, Join Channel", "Cancel"],
5390 )
5391 })?
5392 .await;
5393
5394 if answer == Ok(1) {
5395 return Ok(false);
5396 }
5397 } else {
5398 return Ok(false); // unreachable!() hopefully
5399 }
5400 }
5401
5402 let client = cx.update(|cx| active_call.read(cx).client())?;
5403
5404 let mut client_status = client.status();
5405
5406 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5407 'outer: loop {
5408 let Some(status) = client_status.recv().await else {
5409 return Err(anyhow!("error connecting"));
5410 };
5411
5412 match status {
5413 Status::Connecting
5414 | Status::Authenticating
5415 | Status::Reconnecting
5416 | Status::Reauthenticating => continue,
5417 Status::Connected { .. } => break 'outer,
5418 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5419 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5420 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5421 return Err(ErrorCode::Disconnected.into());
5422 }
5423 }
5424 }
5425
5426 let room = active_call
5427 .update(cx, |active_call, cx| {
5428 active_call.join_channel(channel_id, cx)
5429 })?
5430 .await?;
5431
5432 let Some(room) = room else {
5433 return anyhow::Ok(true);
5434 };
5435
5436 room.update(cx, |room, _| room.room_update_completed())?
5437 .await;
5438
5439 let task = room.update(cx, |room, cx| {
5440 if let Some((project, host)) = room.most_active_project(cx) {
5441 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5442 }
5443
5444 // If you are the first to join a channel, see if you should share your project.
5445 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5446 if let Some(workspace) = requesting_window {
5447 let project = workspace.update(cx, |workspace, cx| {
5448 let project = workspace.project.read(cx);
5449
5450 if !CallSettings::get_global(cx).share_on_join {
5451 return None;
5452 }
5453
5454 if (project.is_local() || project.is_via_ssh())
5455 && project.visible_worktrees(cx).any(|tree| {
5456 tree.read(cx)
5457 .root_entry()
5458 .map_or(false, |entry| entry.is_dir())
5459 })
5460 {
5461 Some(workspace.project.clone())
5462 } else {
5463 None
5464 }
5465 });
5466 if let Ok(Some(project)) = project {
5467 return Some(cx.spawn(|room, mut cx| async move {
5468 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5469 .await?;
5470 Ok(())
5471 }));
5472 }
5473 }
5474 }
5475
5476 None
5477 })?;
5478 if let Some(task) = task {
5479 task.await?;
5480 return anyhow::Ok(true);
5481 }
5482 anyhow::Ok(false)
5483}
5484
5485pub fn join_channel(
5486 channel_id: ChannelId,
5487 app_state: Arc<AppState>,
5488 requesting_window: Option<WindowHandle<Workspace>>,
5489 cx: &mut AppContext,
5490) -> Task<Result<()>> {
5491 let active_call = ActiveCall::global(cx);
5492 cx.spawn(|mut cx| async move {
5493 let result = join_channel_internal(
5494 channel_id,
5495 &app_state,
5496 requesting_window,
5497 &active_call,
5498 &mut cx,
5499 )
5500 .await;
5501
5502 // join channel succeeded, and opened a window
5503 if matches!(result, Ok(true)) {
5504 return anyhow::Ok(());
5505 }
5506
5507 // find an existing workspace to focus and show call controls
5508 let mut active_window =
5509 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5510 if active_window.is_none() {
5511 // no open workspaces, make one to show the error in (blergh)
5512 let (window_handle, _) = cx
5513 .update(|cx| {
5514 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5515 })?
5516 .await?;
5517
5518 if result.is_ok() {
5519 cx.update(|cx| {
5520 cx.dispatch_action(&OpenChannelNotes);
5521 }).log_err();
5522 }
5523
5524 active_window = Some(window_handle);
5525 }
5526
5527 if let Err(err) = result {
5528 log::error!("failed to join channel: {}", err);
5529 if let Some(active_window) = active_window {
5530 active_window
5531 .update(&mut cx, |_, cx| {
5532 let detail: SharedString = match err.error_code() {
5533 ErrorCode::SignedOut => {
5534 "Please sign in to continue.".into()
5535 }
5536 ErrorCode::UpgradeRequired => {
5537 "Your are running an unsupported version of Zed. Please update to continue.".into()
5538 }
5539 ErrorCode::NoSuchChannel => {
5540 "No matching channel was found. Please check the link and try again.".into()
5541 }
5542 ErrorCode::Forbidden => {
5543 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5544 }
5545 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5546 _ => format!("{}\n\nPlease try again.", err).into(),
5547 };
5548 cx.prompt(
5549 PromptLevel::Critical,
5550 "Failed to join channel",
5551 Some(&detail),
5552 &["Ok"],
5553 )
5554 })?
5555 .await
5556 .ok();
5557 }
5558 }
5559
5560 // return ok, we showed the error to the user.
5561 anyhow::Ok(())
5562 })
5563}
5564
5565pub async fn get_any_active_workspace(
5566 app_state: Arc<AppState>,
5567 mut cx: AsyncAppContext,
5568) -> anyhow::Result<WindowHandle<Workspace>> {
5569 // find an existing workspace to focus and show call controls
5570 let active_window = activate_any_workspace_window(&mut cx);
5571 if active_window.is_none() {
5572 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5573 .await?;
5574 }
5575 activate_any_workspace_window(&mut cx).context("could not open zed")
5576}
5577
5578fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5579 cx.update(|cx| {
5580 if let Some(workspace_window) = cx
5581 .active_window()
5582 .and_then(|window| window.downcast::<Workspace>())
5583 {
5584 return Some(workspace_window);
5585 }
5586
5587 for window in cx.windows() {
5588 if let Some(workspace_window) = window.downcast::<Workspace>() {
5589 workspace_window
5590 .update(cx, |_, cx| cx.activate_window())
5591 .ok();
5592 return Some(workspace_window);
5593 }
5594 }
5595 None
5596 })
5597 .ok()
5598 .flatten()
5599}
5600
5601pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5602 cx.windows()
5603 .into_iter()
5604 .filter_map(|window| window.downcast::<Workspace>())
5605 .filter(|workspace| {
5606 workspace
5607 .read(cx)
5608 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5609 })
5610 .collect()
5611}
5612
5613#[derive(Default)]
5614pub struct OpenOptions {
5615 pub open_new_workspace: Option<bool>,
5616 pub replace_window: Option<WindowHandle<Workspace>>,
5617 pub env: Option<HashMap<String, String>>,
5618}
5619
5620#[allow(clippy::type_complexity)]
5621pub fn open_paths(
5622 abs_paths: &[PathBuf],
5623 app_state: Arc<AppState>,
5624 open_options: OpenOptions,
5625 cx: &mut AppContext,
5626) -> Task<
5627 anyhow::Result<(
5628 WindowHandle<Workspace>,
5629 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5630 )>,
5631> {
5632 let abs_paths = abs_paths.to_vec();
5633 let mut existing = None;
5634 let mut best_match = None;
5635 let mut open_visible = OpenVisible::All;
5636
5637 if open_options.open_new_workspace != Some(true) {
5638 for window in local_workspace_windows(cx) {
5639 if let Ok(workspace) = window.read(cx) {
5640 let m = workspace
5641 .project
5642 .read(cx)
5643 .visibility_for_paths(&abs_paths, cx);
5644 if m > best_match {
5645 existing = Some(window);
5646 best_match = m;
5647 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5648 existing = Some(window)
5649 }
5650 }
5651 }
5652 }
5653
5654 cx.spawn(move |mut cx| async move {
5655 if open_options.open_new_workspace.is_none() && existing.is_none() {
5656 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5657 if futures::future::join_all(all_files)
5658 .await
5659 .into_iter()
5660 .filter_map(|result| result.ok().flatten())
5661 .all(|file| !file.is_dir)
5662 {
5663 cx.update(|cx| {
5664 for window in local_workspace_windows(cx) {
5665 if let Ok(workspace) = window.read(cx) {
5666 let project = workspace.project().read(cx);
5667 if project.is_via_collab() {
5668 continue;
5669 }
5670 existing = Some(window);
5671 open_visible = OpenVisible::None;
5672 break;
5673 }
5674 }
5675 })?;
5676 }
5677 }
5678
5679 if let Some(existing) = existing {
5680 let open_task = existing
5681 .update(&mut cx, |workspace, cx| {
5682 cx.activate_window();
5683 workspace.open_paths(abs_paths, open_visible, None, cx)
5684 })?
5685 .await;
5686
5687 _ = existing.update(&mut cx, |workspace, cx| {
5688 for item in open_task.iter().flatten() {
5689 if let Err(e) = item {
5690 workspace.show_error(&e, cx);
5691 }
5692 }
5693 });
5694
5695 Ok((existing, open_task))
5696 } else {
5697 cx.update(move |cx| {
5698 Workspace::new_local(
5699 abs_paths,
5700 app_state.clone(),
5701 open_options.replace_window,
5702 open_options.env,
5703 cx,
5704 )
5705 })?
5706 .await
5707 }
5708 })
5709}
5710
5711pub fn open_new(
5712 open_options: OpenOptions,
5713 app_state: Arc<AppState>,
5714 cx: &mut AppContext,
5715 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5716) -> Task<anyhow::Result<()>> {
5717 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5718 cx.spawn(|mut cx| async move {
5719 let (workspace, opened_paths) = task.await?;
5720 workspace.update(&mut cx, |workspace, cx| {
5721 if opened_paths.is_empty() {
5722 init(workspace, cx)
5723 }
5724 })?;
5725 Ok(())
5726 })
5727}
5728
5729pub fn create_and_open_local_file(
5730 path: &'static Path,
5731 cx: &mut ViewContext<Workspace>,
5732 default_content: impl 'static + Send + FnOnce() -> Rope,
5733) -> Task<Result<Box<dyn ItemHandle>>> {
5734 cx.spawn(|workspace, mut cx| async move {
5735 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5736 if !fs.is_file(path).await {
5737 fs.create_file(path, Default::default()).await?;
5738 fs.save(path, &default_content(), Default::default())
5739 .await?;
5740 }
5741
5742 let mut items = workspace
5743 .update(&mut cx, |workspace, cx| {
5744 workspace.with_local_workspace(cx, |workspace, cx| {
5745 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5746 })
5747 })?
5748 .await?
5749 .await;
5750
5751 let item = items.pop().flatten();
5752 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5753 })
5754}
5755
5756pub fn open_ssh_project(
5757 window: WindowHandle<Workspace>,
5758 connection_options: SshConnectionOptions,
5759 cancel_rx: oneshot::Receiver<()>,
5760 delegate: Arc<dyn SshClientDelegate>,
5761 app_state: Arc<AppState>,
5762 paths: Vec<PathBuf>,
5763 cx: &mut AppContext,
5764) -> Task<Result<()>> {
5765 cx.spawn(|mut cx| async move {
5766 let (serialized_ssh_project, workspace_id, serialized_workspace) =
5767 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5768
5769 let session = match cx
5770 .update(|cx| {
5771 remote::SshRemoteClient::new(
5772 ConnectionIdentifier::Workspace(workspace_id.0),
5773 connection_options,
5774 cancel_rx,
5775 delegate,
5776 cx,
5777 )
5778 })?
5779 .await?
5780 {
5781 Some(result) => result,
5782 None => return Ok(()),
5783 };
5784
5785 let project = cx.update(|cx| {
5786 project::Project::ssh(
5787 session,
5788 app_state.client.clone(),
5789 app_state.node_runtime.clone(),
5790 app_state.user_store.clone(),
5791 app_state.languages.clone(),
5792 app_state.fs.clone(),
5793 cx,
5794 )
5795 })?;
5796
5797 let toolchains = DB.toolchains(workspace_id).await?;
5798 for (toolchain, worktree_id) in toolchains {
5799 project
5800 .update(&mut cx, |this, cx| {
5801 this.activate_toolchain(worktree_id, toolchain, cx)
5802 })?
5803 .await;
5804 }
5805 let mut project_paths_to_open = vec![];
5806 let mut project_path_errors = vec![];
5807
5808 for path in paths {
5809 let result = cx
5810 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5811 .await;
5812 match result {
5813 Ok((_, project_path)) => {
5814 project_paths_to_open.push((path.clone(), Some(project_path)));
5815 }
5816 Err(error) => {
5817 project_path_errors.push(error);
5818 }
5819 };
5820 }
5821
5822 if project_paths_to_open.is_empty() {
5823 return Err(project_path_errors
5824 .pop()
5825 .unwrap_or_else(|| anyhow!("no paths given")));
5826 }
5827
5828 cx.update_window(window.into(), |_, cx| {
5829 cx.replace_root_view(|cx| {
5830 let mut workspace =
5831 Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5832
5833 workspace
5834 .client()
5835 .telemetry()
5836 .report_app_event("open ssh project".to_string());
5837
5838 workspace.set_serialized_ssh_project(serialized_ssh_project);
5839 workspace
5840 });
5841 })?;
5842
5843 window
5844 .update(&mut cx, |_, cx| {
5845 cx.activate_window();
5846
5847 open_items(serialized_workspace, project_paths_to_open, cx)
5848 })?
5849 .await?;
5850
5851 window.update(&mut cx, |workspace, cx| {
5852 for error in project_path_errors {
5853 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5854 if let Some(path) = error.error_tag("path") {
5855 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5856 }
5857 } else {
5858 workspace.show_error(&error, cx)
5859 }
5860 }
5861 })
5862 })
5863}
5864
5865fn serialize_ssh_project(
5866 connection_options: SshConnectionOptions,
5867 paths: Vec<PathBuf>,
5868 cx: &AsyncAppContext,
5869) -> Task<
5870 Result<(
5871 SerializedSshProject,
5872 WorkspaceId,
5873 Option<SerializedWorkspace>,
5874 )>,
5875> {
5876 cx.background_executor().spawn(async move {
5877 let serialized_ssh_project = persistence::DB
5878 .get_or_create_ssh_project(
5879 connection_options.host.clone(),
5880 connection_options.port,
5881 paths
5882 .iter()
5883 .map(|path| path.to_string_lossy().to_string())
5884 .collect::<Vec<_>>(),
5885 connection_options.username.clone(),
5886 )
5887 .await?;
5888
5889 let serialized_workspace =
5890 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5891
5892 let workspace_id = if let Some(workspace_id) =
5893 serialized_workspace.as_ref().map(|workspace| workspace.id)
5894 {
5895 workspace_id
5896 } else {
5897 persistence::DB.next_id().await?
5898 };
5899
5900 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5901 })
5902}
5903
5904pub fn join_in_room_project(
5905 project_id: u64,
5906 follow_user_id: u64,
5907 app_state: Arc<AppState>,
5908 cx: &mut AppContext,
5909) -> Task<Result<()>> {
5910 let windows = cx.windows();
5911 cx.spawn(|mut cx| async move {
5912 let existing_workspace = windows.into_iter().find_map(|window| {
5913 window.downcast::<Workspace>().and_then(|window| {
5914 window
5915 .update(&mut cx, |workspace, cx| {
5916 if workspace.project().read(cx).remote_id() == Some(project_id) {
5917 Some(window)
5918 } else {
5919 None
5920 }
5921 })
5922 .unwrap_or(None)
5923 })
5924 });
5925
5926 let workspace = if let Some(existing_workspace) = existing_workspace {
5927 existing_workspace
5928 } else {
5929 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5930 let room = active_call
5931 .read_with(&cx, |call, _| call.room().cloned())?
5932 .ok_or_else(|| anyhow!("not in a call"))?;
5933 let project = room
5934 .update(&mut cx, |room, cx| {
5935 room.join_project(
5936 project_id,
5937 app_state.languages.clone(),
5938 app_state.fs.clone(),
5939 cx,
5940 )
5941 })?
5942 .await?;
5943
5944 let window_bounds_override = window_bounds_env_override();
5945 cx.update(|cx| {
5946 let mut options = (app_state.build_window_options)(None, cx);
5947 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5948 cx.open_window(options, |cx| {
5949 cx.new_view(|cx| {
5950 Workspace::new(Default::default(), project, app_state.clone(), cx)
5951 })
5952 })
5953 })??
5954 };
5955
5956 workspace.update(&mut cx, |workspace, cx| {
5957 cx.activate(true);
5958 cx.activate_window();
5959
5960 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5961 let follow_peer_id = room
5962 .read(cx)
5963 .remote_participants()
5964 .iter()
5965 .find(|(_, participant)| participant.user.id == follow_user_id)
5966 .map(|(_, p)| p.peer_id)
5967 .or_else(|| {
5968 // If we couldn't follow the given user, follow the host instead.
5969 let collaborator = workspace
5970 .project()
5971 .read(cx)
5972 .collaborators()
5973 .values()
5974 .find(|collaborator| collaborator.is_host)?;
5975 Some(collaborator.peer_id)
5976 });
5977
5978 if let Some(follow_peer_id) = follow_peer_id {
5979 workspace.follow(follow_peer_id, cx);
5980 }
5981 }
5982 })?;
5983
5984 anyhow::Ok(())
5985 })
5986}
5987
5988pub fn reload(reload: &Reload, cx: &mut AppContext) {
5989 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5990 let mut workspace_windows = cx
5991 .windows()
5992 .into_iter()
5993 .filter_map(|window| window.downcast::<Workspace>())
5994 .collect::<Vec<_>>();
5995
5996 // If multiple windows have unsaved changes, and need a save prompt,
5997 // prompt in the active window before switching to a different window.
5998 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5999
6000 let mut prompt = None;
6001 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6002 prompt = window
6003 .update(cx, |_, cx| {
6004 cx.prompt(
6005 PromptLevel::Info,
6006 "Are you sure you want to restart?",
6007 None,
6008 &["Restart", "Cancel"],
6009 )
6010 })
6011 .ok();
6012 }
6013
6014 let binary_path = reload.binary_path.clone();
6015 cx.spawn(|mut cx| async move {
6016 if let Some(prompt) = prompt {
6017 let answer = prompt.await?;
6018 if answer != 0 {
6019 return Ok(());
6020 }
6021 }
6022
6023 // If the user cancels any save prompt, then keep the app open.
6024 for window in workspace_windows {
6025 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
6026 workspace.prepare_to_close(CloseIntent::Quit, cx)
6027 }) {
6028 if !should_close.await? {
6029 return Ok(());
6030 }
6031 }
6032 }
6033
6034 cx.update(|cx| cx.restart(binary_path))
6035 })
6036 .detach_and_log_err(cx);
6037}
6038
6039fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6040 let mut parts = value.split(',');
6041 let x: usize = parts.next()?.parse().ok()?;
6042 let y: usize = parts.next()?.parse().ok()?;
6043 Some(point(px(x as f32), px(y as f32)))
6044}
6045
6046fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6047 let mut parts = value.split(',');
6048 let width: usize = parts.next()?.parse().ok()?;
6049 let height: usize = parts.next()?.parse().ok()?;
6050 Some(size(px(width as f32), px(height as f32)))
6051}
6052
6053pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
6054 const BORDER_SIZE: Pixels = px(1.0);
6055 let decorations = cx.window_decorations();
6056
6057 if matches!(decorations, Decorations::Client { .. }) {
6058 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6059 }
6060
6061 struct GlobalResizeEdge(ResizeEdge);
6062 impl Global for GlobalResizeEdge {}
6063
6064 div()
6065 .id("window-backdrop")
6066 .bg(transparent_black())
6067 .map(|div| match decorations {
6068 Decorations::Server => div,
6069 Decorations::Client { tiling, .. } => div
6070 .when(!(tiling.top || tiling.right), |div| {
6071 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6072 })
6073 .when(!(tiling.top || tiling.left), |div| {
6074 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6075 })
6076 .when(!(tiling.bottom || tiling.right), |div| {
6077 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6078 })
6079 .when(!(tiling.bottom || tiling.left), |div| {
6080 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6081 })
6082 .when(!tiling.top, |div| {
6083 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6084 })
6085 .when(!tiling.bottom, |div| {
6086 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6087 })
6088 .when(!tiling.left, |div| {
6089 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6090 })
6091 .when(!tiling.right, |div| {
6092 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6093 })
6094 .on_mouse_move(move |e, cx| {
6095 let size = cx.window_bounds().get_bounds().size;
6096 let pos = e.position;
6097
6098 let new_edge =
6099 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6100
6101 let edge = cx.try_global::<GlobalResizeEdge>();
6102 if new_edge != edge.map(|edge| edge.0) {
6103 cx.window_handle()
6104 .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
6105 .ok();
6106 }
6107 })
6108 .on_mouse_down(MouseButton::Left, move |e, cx| {
6109 let size = cx.window_bounds().get_bounds().size;
6110 let pos = e.position;
6111
6112 let edge = match resize_edge(
6113 pos,
6114 theme::CLIENT_SIDE_DECORATION_SHADOW,
6115 size,
6116 tiling,
6117 ) {
6118 Some(value) => value,
6119 None => return,
6120 };
6121
6122 cx.start_window_resize(edge);
6123 }),
6124 })
6125 .size_full()
6126 .child(
6127 div()
6128 .cursor(CursorStyle::Arrow)
6129 .map(|div| match decorations {
6130 Decorations::Server => div,
6131 Decorations::Client { tiling } => div
6132 .border_color(cx.theme().colors().border)
6133 .when(!(tiling.top || tiling.right), |div| {
6134 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6135 })
6136 .when(!(tiling.top || tiling.left), |div| {
6137 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6138 })
6139 .when(!(tiling.bottom || tiling.right), |div| {
6140 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6141 })
6142 .when(!(tiling.bottom || tiling.left), |div| {
6143 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6144 })
6145 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6146 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6147 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6148 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6149 .when(!tiling.is_tiled(), |div| {
6150 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6151 color: Hsla {
6152 h: 0.,
6153 s: 0.,
6154 l: 0.,
6155 a: 0.4,
6156 },
6157 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6158 spread_radius: px(0.),
6159 offset: point(px(0.0), px(0.0)),
6160 }])
6161 }),
6162 })
6163 .on_mouse_move(|_e, cx| {
6164 cx.stop_propagation();
6165 })
6166 .size_full()
6167 .child(element),
6168 )
6169 .map(|div| match decorations {
6170 Decorations::Server => div,
6171 Decorations::Client { tiling, .. } => div.child(
6172 canvas(
6173 |_bounds, cx| {
6174 cx.insert_hitbox(
6175 Bounds::new(
6176 point(px(0.0), px(0.0)),
6177 cx.window_bounds().get_bounds().size,
6178 ),
6179 false,
6180 )
6181 },
6182 move |_bounds, hitbox, cx| {
6183 let mouse = cx.mouse_position();
6184 let size = cx.window_bounds().get_bounds().size;
6185 let Some(edge) =
6186 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6187 else {
6188 return;
6189 };
6190 cx.set_global(GlobalResizeEdge(edge));
6191 cx.set_cursor_style(
6192 match edge {
6193 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6194 ResizeEdge::Left | ResizeEdge::Right => {
6195 CursorStyle::ResizeLeftRight
6196 }
6197 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6198 CursorStyle::ResizeUpLeftDownRight
6199 }
6200 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6201 CursorStyle::ResizeUpRightDownLeft
6202 }
6203 },
6204 &hitbox,
6205 );
6206 },
6207 )
6208 .size_full()
6209 .absolute(),
6210 ),
6211 })
6212}
6213
6214fn resize_edge(
6215 pos: Point<Pixels>,
6216 shadow_size: Pixels,
6217 window_size: Size<Pixels>,
6218 tiling: Tiling,
6219) -> Option<ResizeEdge> {
6220 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6221 if bounds.contains(&pos) {
6222 return None;
6223 }
6224
6225 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6226 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6227 if !tiling.top && top_left_bounds.contains(&pos) {
6228 return Some(ResizeEdge::TopLeft);
6229 }
6230
6231 let top_right_bounds = Bounds::new(
6232 Point::new(window_size.width - corner_size.width, px(0.)),
6233 corner_size,
6234 );
6235 if !tiling.top && top_right_bounds.contains(&pos) {
6236 return Some(ResizeEdge::TopRight);
6237 }
6238
6239 let bottom_left_bounds = Bounds::new(
6240 Point::new(px(0.), window_size.height - corner_size.height),
6241 corner_size,
6242 );
6243 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6244 return Some(ResizeEdge::BottomLeft);
6245 }
6246
6247 let bottom_right_bounds = Bounds::new(
6248 Point::new(
6249 window_size.width - corner_size.width,
6250 window_size.height - corner_size.height,
6251 ),
6252 corner_size,
6253 );
6254 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6255 return Some(ResizeEdge::BottomRight);
6256 }
6257
6258 if !tiling.top && pos.y < shadow_size {
6259 Some(ResizeEdge::Top)
6260 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6261 Some(ResizeEdge::Bottom)
6262 } else if !tiling.left && pos.x < shadow_size {
6263 Some(ResizeEdge::Left)
6264 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6265 Some(ResizeEdge::Right)
6266 } else {
6267 None
6268 }
6269}
6270
6271fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext) {
6272 if pane == active_pane {
6273 return;
6274 } else if pane.read(cx).items_len() == 0 {
6275 pane.update(cx, |_, cx| {
6276 cx.emit(pane::Event::Remove {
6277 focus_on_pane: None,
6278 });
6279 })
6280 } else {
6281 move_all_items(pane, active_pane, cx);
6282 }
6283}
6284
6285fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext) {
6286 let destination_is_different = from_pane != to_pane;
6287 let mut moved_items = 0;
6288 for (item_ix, item_handle) in from_pane
6289 .read(cx)
6290 .items()
6291 .enumerate()
6292 .map(|(ix, item)| (ix, item.clone()))
6293 .collect::<Vec<_>>()
6294 {
6295 let ix = item_ix - moved_items;
6296 if destination_is_different {
6297 // Close item from previous pane
6298 from_pane.update(cx, |source, cx| {
6299 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6300 });
6301 moved_items += 1;
6302 }
6303
6304 // This automatically removes duplicate items in the pane
6305 to_pane.update(cx, |destination, cx| {
6306 destination.add_item(item_handle, true, true, None, cx);
6307 destination.focus(cx)
6308 });
6309 }
6310}
6311
6312pub fn move_item(
6313 source: &View<Pane>,
6314 destination: &View<Pane>,
6315 item_id_to_move: EntityId,
6316 destination_index: usize,
6317 cx: &mut WindowContext,
6318) {
6319 let Some((item_ix, item_handle)) = source
6320 .read(cx)
6321 .items()
6322 .enumerate()
6323 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6324 .map(|(ix, item)| (ix, item.clone()))
6325 else {
6326 // Tab was closed during drag
6327 return;
6328 };
6329
6330 if source != destination {
6331 // Close item from previous pane
6332 source.update(cx, |source, cx| {
6333 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6334 });
6335 }
6336
6337 // This automatically removes duplicate items in the pane
6338 destination.update(cx, |destination, cx| {
6339 destination.add_item(item_handle, true, true, Some(destination_index), cx);
6340 destination.focus(cx)
6341 });
6342}
6343
6344pub fn move_active_item(
6345 source: &View<Pane>,
6346 destination: &View<Pane>,
6347 focus_destination: bool,
6348 close_if_empty: bool,
6349 cx: &mut WindowContext,
6350) {
6351 if source == destination {
6352 return;
6353 }
6354 let Some(active_item) = source.read(cx).active_item() else {
6355 return;
6356 };
6357 source.update(cx, |source_pane, cx| {
6358 let item_id = active_item.item_id();
6359 source_pane.remove_item(item_id, false, close_if_empty, cx);
6360 destination.update(cx, |target_pane, cx| {
6361 target_pane.add_item(
6362 active_item,
6363 focus_destination,
6364 focus_destination,
6365 Some(target_pane.items_len()),
6366 cx,
6367 );
6368 });
6369 });
6370}
6371
6372#[cfg(test)]
6373mod tests {
6374 use std::{cell::RefCell, rc::Rc};
6375
6376 use super::*;
6377 use crate::{
6378 dock::{test::TestPanel, PanelEvent},
6379 item::{
6380 test::{TestItem, TestProjectItem},
6381 ItemEvent,
6382 },
6383 };
6384 use fs::FakeFs;
6385 use gpui::{
6386 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6387 UpdateGlobal, VisualTestContext,
6388 };
6389 use project::{Project, ProjectEntryId};
6390 use serde_json::json;
6391 use settings::SettingsStore;
6392
6393 #[gpui::test]
6394 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6395 init_test(cx);
6396
6397 let fs = FakeFs::new(cx.executor());
6398 let project = Project::test(fs, [], cx).await;
6399 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6400
6401 // Adding an item with no ambiguity renders the tab without detail.
6402 let item1 = cx.new_view(|cx| {
6403 let mut item = TestItem::new(cx);
6404 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6405 item
6406 });
6407 workspace.update(cx, |workspace, cx| {
6408 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6409 });
6410 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6411
6412 // Adding an item that creates ambiguity increases the level of detail on
6413 // both tabs.
6414 let item2 = cx.new_view(|cx| {
6415 let mut item = TestItem::new(cx);
6416 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6417 item
6418 });
6419 workspace.update(cx, |workspace, cx| {
6420 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6421 });
6422 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6423 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6424
6425 // Adding an item that creates ambiguity increases the level of detail only
6426 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6427 // we stop at the highest detail available.
6428 let item3 = cx.new_view(|cx| {
6429 let mut item = TestItem::new(cx);
6430 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6431 item
6432 });
6433 workspace.update(cx, |workspace, cx| {
6434 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6435 });
6436 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6437 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6438 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6439 }
6440
6441 #[gpui::test]
6442 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6443 init_test(cx);
6444
6445 let fs = FakeFs::new(cx.executor());
6446 fs.insert_tree(
6447 "/root1",
6448 json!({
6449 "one.txt": "",
6450 "two.txt": "",
6451 }),
6452 )
6453 .await;
6454 fs.insert_tree(
6455 "/root2",
6456 json!({
6457 "three.txt": "",
6458 }),
6459 )
6460 .await;
6461
6462 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6463 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6464 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6465 let worktree_id = project.update(cx, |project, cx| {
6466 project.worktrees(cx).next().unwrap().read(cx).id()
6467 });
6468
6469 let item1 = cx.new_view(|cx| {
6470 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6471 });
6472 let item2 = cx.new_view(|cx| {
6473 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6474 });
6475
6476 // Add an item to an empty pane
6477 workspace.update(cx, |workspace, cx| {
6478 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6479 });
6480 project.update(cx, |project, cx| {
6481 assert_eq!(
6482 project.active_entry(),
6483 project
6484 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6485 .map(|e| e.id)
6486 );
6487 });
6488 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6489
6490 // Add a second item to a non-empty pane
6491 workspace.update(cx, |workspace, cx| {
6492 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6493 });
6494 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6495 project.update(cx, |project, cx| {
6496 assert_eq!(
6497 project.active_entry(),
6498 project
6499 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6500 .map(|e| e.id)
6501 );
6502 });
6503
6504 // Close the active item
6505 pane.update(cx, |pane, cx| {
6506 pane.close_active_item(&Default::default(), cx).unwrap()
6507 })
6508 .await
6509 .unwrap();
6510 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6511 project.update(cx, |project, cx| {
6512 assert_eq!(
6513 project.active_entry(),
6514 project
6515 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6516 .map(|e| e.id)
6517 );
6518 });
6519
6520 // Add a project folder
6521 project
6522 .update(cx, |project, cx| {
6523 project.find_or_create_worktree("root2", true, cx)
6524 })
6525 .await
6526 .unwrap();
6527 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6528
6529 // Remove a project folder
6530 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6531 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6532 }
6533
6534 #[gpui::test]
6535 async fn test_close_window(cx: &mut TestAppContext) {
6536 init_test(cx);
6537
6538 let fs = FakeFs::new(cx.executor());
6539 fs.insert_tree("/root", json!({ "one": "" })).await;
6540
6541 let project = Project::test(fs, ["root".as_ref()], cx).await;
6542 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6543
6544 // When there are no dirty items, there's nothing to do.
6545 let item1 = cx.new_view(TestItem::new);
6546 workspace.update(cx, |w, cx| {
6547 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6548 });
6549 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6550 assert!(task.await.unwrap());
6551
6552 // When there are dirty untitled items, prompt to save each one. If the user
6553 // cancels any prompt, then abort.
6554 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6555 let item3 = cx.new_view(|cx| {
6556 TestItem::new(cx)
6557 .with_dirty(true)
6558 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6559 });
6560 workspace.update(cx, |w, cx| {
6561 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6562 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6563 });
6564 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6565 cx.executor().run_until_parked();
6566 cx.simulate_prompt_answer(2); // cancel save all
6567 cx.executor().run_until_parked();
6568 cx.simulate_prompt_answer(2); // cancel save all
6569 cx.executor().run_until_parked();
6570 assert!(!cx.has_pending_prompt());
6571 assert!(!task.await.unwrap());
6572 }
6573
6574 #[gpui::test]
6575 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6576 init_test(cx);
6577
6578 // Register TestItem as a serializable item
6579 cx.update(|cx| {
6580 register_serializable_item::<TestItem>(cx);
6581 });
6582
6583 let fs = FakeFs::new(cx.executor());
6584 fs.insert_tree("/root", json!({ "one": "" })).await;
6585
6586 let project = Project::test(fs, ["root".as_ref()], cx).await;
6587 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6588
6589 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6590 let item1 = cx.new_view(|cx| {
6591 TestItem::new(cx)
6592 .with_dirty(true)
6593 .with_serialize(|| Some(Task::ready(Ok(()))))
6594 });
6595 let item2 = cx.new_view(|cx| {
6596 TestItem::new(cx)
6597 .with_dirty(true)
6598 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6599 .with_serialize(|| Some(Task::ready(Ok(()))))
6600 });
6601 workspace.update(cx, |w, cx| {
6602 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6603 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6604 });
6605 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6606 assert!(task.await.unwrap());
6607 }
6608
6609 #[gpui::test]
6610 async fn test_close_pane_items(cx: &mut TestAppContext) {
6611 init_test(cx);
6612
6613 let fs = FakeFs::new(cx.executor());
6614
6615 let project = Project::test(fs, None, cx).await;
6616 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6617
6618 let item1 = cx.new_view(|cx| {
6619 TestItem::new(cx)
6620 .with_dirty(true)
6621 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6622 });
6623 let item2 = cx.new_view(|cx| {
6624 TestItem::new(cx)
6625 .with_dirty(true)
6626 .with_conflict(true)
6627 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6628 });
6629 let item3 = cx.new_view(|cx| {
6630 TestItem::new(cx)
6631 .with_dirty(true)
6632 .with_conflict(true)
6633 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6634 });
6635 let item4 = cx.new_view(|cx| {
6636 TestItem::new(cx).with_dirty(true).with_project_items(&[{
6637 let project_item = TestProjectItem::new_untitled(cx);
6638 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6639 project_item
6640 }])
6641 });
6642 let pane = workspace.update(cx, |workspace, cx| {
6643 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6644 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6645 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6646 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6647 workspace.active_pane().clone()
6648 });
6649
6650 let close_items = pane.update(cx, |pane, cx| {
6651 pane.activate_item(1, true, true, cx);
6652 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6653 let item1_id = item1.item_id();
6654 let item3_id = item3.item_id();
6655 let item4_id = item4.item_id();
6656 pane.close_items(cx, SaveIntent::Close, move |id| {
6657 [item1_id, item3_id, item4_id].contains(&id)
6658 })
6659 });
6660 cx.executor().run_until_parked();
6661
6662 assert!(cx.has_pending_prompt());
6663 // Ignore "Save all" prompt
6664 cx.simulate_prompt_answer(2);
6665 cx.executor().run_until_parked();
6666 // There's a prompt to save item 1.
6667 pane.update(cx, |pane, _| {
6668 assert_eq!(pane.items_len(), 4);
6669 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6670 });
6671 // Confirm saving item 1.
6672 cx.simulate_prompt_answer(0);
6673 cx.executor().run_until_parked();
6674
6675 // Item 1 is saved. There's a prompt to save item 3.
6676 pane.update(cx, |pane, cx| {
6677 assert_eq!(item1.read(cx).save_count, 1);
6678 assert_eq!(item1.read(cx).save_as_count, 0);
6679 assert_eq!(item1.read(cx).reload_count, 0);
6680 assert_eq!(pane.items_len(), 3);
6681 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6682 });
6683 assert!(cx.has_pending_prompt());
6684
6685 // Cancel saving item 3.
6686 cx.simulate_prompt_answer(1);
6687 cx.executor().run_until_parked();
6688
6689 // Item 3 is reloaded. There's a prompt to save item 4.
6690 pane.update(cx, |pane, cx| {
6691 assert_eq!(item3.read(cx).save_count, 0);
6692 assert_eq!(item3.read(cx).save_as_count, 0);
6693 assert_eq!(item3.read(cx).reload_count, 1);
6694 assert_eq!(pane.items_len(), 2);
6695 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6696 });
6697 assert!(cx.has_pending_prompt());
6698
6699 // Confirm saving item 4.
6700 cx.simulate_prompt_answer(0);
6701 cx.executor().run_until_parked();
6702
6703 // There's a prompt for a path for item 4.
6704 cx.simulate_new_path_selection(|_| Some(Default::default()));
6705 close_items.await.unwrap();
6706
6707 // The requested items are closed.
6708 pane.update(cx, |pane, cx| {
6709 assert_eq!(item4.read(cx).save_count, 0);
6710 assert_eq!(item4.read(cx).save_as_count, 1);
6711 assert_eq!(item4.read(cx).reload_count, 0);
6712 assert_eq!(pane.items_len(), 1);
6713 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6714 });
6715 }
6716
6717 #[gpui::test]
6718 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6719 init_test(cx);
6720
6721 let fs = FakeFs::new(cx.executor());
6722 let project = Project::test(fs, [], cx).await;
6723 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6724
6725 // Create several workspace items with single project entries, and two
6726 // workspace items with multiple project entries.
6727 let single_entry_items = (0..=4)
6728 .map(|project_entry_id| {
6729 cx.new_view(|cx| {
6730 TestItem::new(cx)
6731 .with_dirty(true)
6732 .with_project_items(&[dirty_project_item(
6733 project_entry_id,
6734 &format!("{project_entry_id}.txt"),
6735 cx,
6736 )])
6737 })
6738 })
6739 .collect::<Vec<_>>();
6740 let item_2_3 = cx.new_view(|cx| {
6741 TestItem::new(cx)
6742 .with_dirty(true)
6743 .with_singleton(false)
6744 .with_project_items(&[
6745 single_entry_items[2].read(cx).project_items[0].clone(),
6746 single_entry_items[3].read(cx).project_items[0].clone(),
6747 ])
6748 });
6749 let item_3_4 = cx.new_view(|cx| {
6750 TestItem::new(cx)
6751 .with_dirty(true)
6752 .with_singleton(false)
6753 .with_project_items(&[
6754 single_entry_items[3].read(cx).project_items[0].clone(),
6755 single_entry_items[4].read(cx).project_items[0].clone(),
6756 ])
6757 });
6758
6759 // Create two panes that contain the following project entries:
6760 // left pane:
6761 // multi-entry items: (2, 3)
6762 // single-entry items: 0, 1, 2, 3, 4
6763 // right pane:
6764 // single-entry items: 1
6765 // multi-entry items: (3, 4)
6766 let left_pane = workspace.update(cx, |workspace, cx| {
6767 let left_pane = workspace.active_pane().clone();
6768 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6769 for item in single_entry_items {
6770 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6771 }
6772 left_pane.update(cx, |pane, cx| {
6773 pane.activate_item(2, true, true, cx);
6774 });
6775
6776 let right_pane = workspace
6777 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6778 .unwrap();
6779
6780 right_pane.update(cx, |pane, cx| {
6781 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6782 });
6783
6784 left_pane
6785 });
6786
6787 cx.focus_view(&left_pane);
6788
6789 // When closing all of the items in the left pane, we should be prompted twice:
6790 // once for project entry 0, and once for project entry 2. Project entries 1,
6791 // 3, and 4 are all still open in the other paten. After those two
6792 // prompts, the task should complete.
6793
6794 let close = left_pane.update(cx, |pane, cx| {
6795 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6796 });
6797 cx.executor().run_until_parked();
6798
6799 // Discard "Save all" prompt
6800 cx.simulate_prompt_answer(2);
6801
6802 cx.executor().run_until_parked();
6803 left_pane.update(cx, |pane, cx| {
6804 assert_eq!(
6805 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6806 &[ProjectEntryId::from_proto(0)]
6807 );
6808 });
6809 cx.simulate_prompt_answer(0);
6810
6811 cx.executor().run_until_parked();
6812 left_pane.update(cx, |pane, cx| {
6813 assert_eq!(
6814 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6815 &[ProjectEntryId::from_proto(2)]
6816 );
6817 });
6818 cx.simulate_prompt_answer(0);
6819
6820 cx.executor().run_until_parked();
6821 close.await.unwrap();
6822 left_pane.update(cx, |pane, _| {
6823 assert_eq!(pane.items_len(), 0);
6824 });
6825 }
6826
6827 #[gpui::test]
6828 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6829 init_test(cx);
6830
6831 let fs = FakeFs::new(cx.executor());
6832 let project = Project::test(fs, [], cx).await;
6833 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6834 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6835
6836 let item = cx.new_view(|cx| {
6837 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6838 });
6839 let item_id = item.entity_id();
6840 workspace.update(cx, |workspace, cx| {
6841 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6842 });
6843
6844 // Autosave on window change.
6845 item.update(cx, |item, cx| {
6846 SettingsStore::update_global(cx, |settings, cx| {
6847 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6848 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6849 })
6850 });
6851 item.is_dirty = true;
6852 });
6853
6854 // Deactivating the window saves the file.
6855 cx.deactivate_window();
6856 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6857
6858 // Re-activating the window doesn't save the file.
6859 cx.update(|cx| cx.activate_window());
6860 cx.executor().run_until_parked();
6861 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6862
6863 // Autosave on focus change.
6864 item.update(cx, |item, cx| {
6865 cx.focus_self();
6866 SettingsStore::update_global(cx, |settings, cx| {
6867 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6868 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6869 })
6870 });
6871 item.is_dirty = true;
6872 });
6873
6874 // Blurring the item saves the file.
6875 item.update(cx, |_, cx| cx.blur());
6876 cx.executor().run_until_parked();
6877 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6878
6879 // Deactivating the window still saves the file.
6880 item.update(cx, |item, cx| {
6881 cx.focus_self();
6882 item.is_dirty = true;
6883 });
6884 cx.deactivate_window();
6885 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6886
6887 // Autosave after delay.
6888 item.update(cx, |item, cx| {
6889 SettingsStore::update_global(cx, |settings, cx| {
6890 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6891 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6892 })
6893 });
6894 item.is_dirty = true;
6895 cx.emit(ItemEvent::Edit);
6896 });
6897
6898 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6899 cx.executor().advance_clock(Duration::from_millis(250));
6900 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6901
6902 // After delay expires, the file is saved.
6903 cx.executor().advance_clock(Duration::from_millis(250));
6904 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6905
6906 // Autosave on focus change, ensuring closing the tab counts as such.
6907 item.update(cx, |item, cx| {
6908 SettingsStore::update_global(cx, |settings, cx| {
6909 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6910 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6911 })
6912 });
6913 item.is_dirty = true;
6914 for project_item in &mut item.project_items {
6915 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6916 }
6917 });
6918
6919 pane.update(cx, |pane, cx| {
6920 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6921 })
6922 .await
6923 .unwrap();
6924 assert!(!cx.has_pending_prompt());
6925 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6926
6927 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6928 workspace.update(cx, |workspace, cx| {
6929 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6930 });
6931 item.update(cx, |item, cx| {
6932 item.project_items[0].update(cx, |item, _| {
6933 item.entry_id = None;
6934 });
6935 item.is_dirty = true;
6936 cx.blur();
6937 });
6938 cx.run_until_parked();
6939 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6940
6941 // Ensure autosave is prevented for deleted files also when closing the buffer.
6942 let _close_items = pane.update(cx, |pane, cx| {
6943 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6944 });
6945 cx.run_until_parked();
6946 assert!(cx.has_pending_prompt());
6947 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6948 }
6949
6950 #[gpui::test]
6951 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6952 init_test(cx);
6953
6954 let fs = FakeFs::new(cx.executor());
6955
6956 let project = Project::test(fs, [], cx).await;
6957 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6958
6959 let item = cx.new_view(|cx| {
6960 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6961 });
6962 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6963 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6964 let toolbar_notify_count = Rc::new(RefCell::new(0));
6965
6966 workspace.update(cx, |workspace, cx| {
6967 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6968 let toolbar_notification_count = toolbar_notify_count.clone();
6969 cx.observe(&toolbar, move |_, _, _| {
6970 *toolbar_notification_count.borrow_mut() += 1
6971 })
6972 .detach();
6973 });
6974
6975 pane.update(cx, |pane, _| {
6976 assert!(!pane.can_navigate_backward());
6977 assert!(!pane.can_navigate_forward());
6978 });
6979
6980 item.update(cx, |item, cx| {
6981 item.set_state("one".to_string(), cx);
6982 });
6983
6984 // Toolbar must be notified to re-render the navigation buttons
6985 assert_eq!(*toolbar_notify_count.borrow(), 1);
6986
6987 pane.update(cx, |pane, _| {
6988 assert!(pane.can_navigate_backward());
6989 assert!(!pane.can_navigate_forward());
6990 });
6991
6992 workspace
6993 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6994 .await
6995 .unwrap();
6996
6997 assert_eq!(*toolbar_notify_count.borrow(), 2);
6998 pane.update(cx, |pane, _| {
6999 assert!(!pane.can_navigate_backward());
7000 assert!(pane.can_navigate_forward());
7001 });
7002 }
7003
7004 #[gpui::test]
7005 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7006 init_test(cx);
7007 let fs = FakeFs::new(cx.executor());
7008
7009 let project = Project::test(fs, [], cx).await;
7010 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7011
7012 let panel = workspace.update(cx, |workspace, cx| {
7013 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7014 workspace.add_panel(panel.clone(), cx);
7015
7016 workspace
7017 .right_dock()
7018 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7019
7020 panel
7021 });
7022
7023 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7024 pane.update(cx, |pane, cx| {
7025 let item = cx.new_view(TestItem::new);
7026 pane.add_item(Box::new(item), true, true, None, cx);
7027 });
7028
7029 // Transfer focus from center to panel
7030 workspace.update(cx, |workspace, cx| {
7031 workspace.toggle_panel_focus::<TestPanel>(cx);
7032 });
7033
7034 workspace.update(cx, |workspace, cx| {
7035 assert!(workspace.right_dock().read(cx).is_open());
7036 assert!(!panel.is_zoomed(cx));
7037 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7038 });
7039
7040 // Transfer focus from panel to center
7041 workspace.update(cx, |workspace, cx| {
7042 workspace.toggle_panel_focus::<TestPanel>(cx);
7043 });
7044
7045 workspace.update(cx, |workspace, cx| {
7046 assert!(workspace.right_dock().read(cx).is_open());
7047 assert!(!panel.is_zoomed(cx));
7048 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7049 });
7050
7051 // Close the dock
7052 workspace.update(cx, |workspace, cx| {
7053 workspace.toggle_dock(DockPosition::Right, cx);
7054 });
7055
7056 workspace.update(cx, |workspace, cx| {
7057 assert!(!workspace.right_dock().read(cx).is_open());
7058 assert!(!panel.is_zoomed(cx));
7059 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7060 });
7061
7062 // Open the dock
7063 workspace.update(cx, |workspace, cx| {
7064 workspace.toggle_dock(DockPosition::Right, cx);
7065 });
7066
7067 workspace.update(cx, |workspace, cx| {
7068 assert!(workspace.right_dock().read(cx).is_open());
7069 assert!(!panel.is_zoomed(cx));
7070 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7071 });
7072
7073 // Focus and zoom panel
7074 panel.update(cx, |panel, cx| {
7075 cx.focus_self();
7076 panel.set_zoomed(true, cx)
7077 });
7078
7079 workspace.update(cx, |workspace, cx| {
7080 assert!(workspace.right_dock().read(cx).is_open());
7081 assert!(panel.is_zoomed(cx));
7082 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7083 });
7084
7085 // Transfer focus to the center closes the dock
7086 workspace.update(cx, |workspace, cx| {
7087 workspace.toggle_panel_focus::<TestPanel>(cx);
7088 });
7089
7090 workspace.update(cx, |workspace, cx| {
7091 assert!(!workspace.right_dock().read(cx).is_open());
7092 assert!(panel.is_zoomed(cx));
7093 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7094 });
7095
7096 // Transferring focus back to the panel keeps it zoomed
7097 workspace.update(cx, |workspace, cx| {
7098 workspace.toggle_panel_focus::<TestPanel>(cx);
7099 });
7100
7101 workspace.update(cx, |workspace, cx| {
7102 assert!(workspace.right_dock().read(cx).is_open());
7103 assert!(panel.is_zoomed(cx));
7104 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7105 });
7106
7107 // Close the dock while it is zoomed
7108 workspace.update(cx, |workspace, cx| {
7109 workspace.toggle_dock(DockPosition::Right, cx)
7110 });
7111
7112 workspace.update(cx, |workspace, cx| {
7113 assert!(!workspace.right_dock().read(cx).is_open());
7114 assert!(panel.is_zoomed(cx));
7115 assert!(workspace.zoomed.is_none());
7116 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7117 });
7118
7119 // Opening the dock, when it's zoomed, retains focus
7120 workspace.update(cx, |workspace, cx| {
7121 workspace.toggle_dock(DockPosition::Right, cx)
7122 });
7123
7124 workspace.update(cx, |workspace, cx| {
7125 assert!(workspace.right_dock().read(cx).is_open());
7126 assert!(panel.is_zoomed(cx));
7127 assert!(workspace.zoomed.is_some());
7128 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7129 });
7130
7131 // Unzoom and close the panel, zoom the active pane.
7132 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
7133 workspace.update(cx, |workspace, cx| {
7134 workspace.toggle_dock(DockPosition::Right, cx)
7135 });
7136 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
7137
7138 // Opening a dock unzooms the pane.
7139 workspace.update(cx, |workspace, cx| {
7140 workspace.toggle_dock(DockPosition::Right, cx)
7141 });
7142 workspace.update(cx, |workspace, cx| {
7143 let pane = pane.read(cx);
7144 assert!(!pane.is_zoomed());
7145 assert!(!pane.focus_handle(cx).is_focused(cx));
7146 assert!(workspace.right_dock().read(cx).is_open());
7147 assert!(workspace.zoomed.is_none());
7148 });
7149 }
7150
7151 #[gpui::test]
7152 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7153 init_test(cx);
7154
7155 let fs = FakeFs::new(cx.executor());
7156
7157 let project = Project::test(fs, None, cx).await;
7158 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7159
7160 // Let's arrange the panes like this:
7161 //
7162 // +-----------------------+
7163 // | top |
7164 // +------+--------+-------+
7165 // | left | center | right |
7166 // +------+--------+-------+
7167 // | bottom |
7168 // +-----------------------+
7169
7170 let top_item = cx.new_view(|cx| {
7171 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7172 });
7173 let bottom_item = cx.new_view(|cx| {
7174 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7175 });
7176 let left_item = cx.new_view(|cx| {
7177 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7178 });
7179 let right_item = cx.new_view(|cx| {
7180 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7181 });
7182 let center_item = cx.new_view(|cx| {
7183 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7184 });
7185
7186 let top_pane_id = workspace.update(cx, |workspace, cx| {
7187 let top_pane_id = workspace.active_pane().entity_id();
7188 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
7189 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
7190 top_pane_id
7191 });
7192 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
7193 let bottom_pane_id = workspace.active_pane().entity_id();
7194 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
7195 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
7196 bottom_pane_id
7197 });
7198 let left_pane_id = workspace.update(cx, |workspace, cx| {
7199 let left_pane_id = workspace.active_pane().entity_id();
7200 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
7201 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7202 left_pane_id
7203 });
7204 let right_pane_id = workspace.update(cx, |workspace, cx| {
7205 let right_pane_id = workspace.active_pane().entity_id();
7206 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
7207 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
7208 right_pane_id
7209 });
7210 let center_pane_id = workspace.update(cx, |workspace, cx| {
7211 let center_pane_id = workspace.active_pane().entity_id();
7212 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
7213 center_pane_id
7214 });
7215 cx.executor().run_until_parked();
7216
7217 workspace.update(cx, |workspace, cx| {
7218 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7219
7220 // Join into next from center pane into right
7221 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7222 });
7223
7224 workspace.update(cx, |workspace, cx| {
7225 let active_pane = workspace.active_pane();
7226 assert_eq!(right_pane_id, active_pane.entity_id());
7227 assert_eq!(2, active_pane.read(cx).items_len());
7228 let item_ids_in_pane =
7229 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7230 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7231 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7232
7233 // Join into next from right pane into bottom
7234 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7235 });
7236
7237 workspace.update(cx, |workspace, cx| {
7238 let active_pane = workspace.active_pane();
7239 assert_eq!(bottom_pane_id, active_pane.entity_id());
7240 assert_eq!(3, active_pane.read(cx).items_len());
7241 let item_ids_in_pane =
7242 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7243 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7244 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7245 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7246
7247 // Join into next from bottom pane into left
7248 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7249 });
7250
7251 workspace.update(cx, |workspace, cx| {
7252 let active_pane = workspace.active_pane();
7253 assert_eq!(left_pane_id, active_pane.entity_id());
7254 assert_eq!(4, active_pane.read(cx).items_len());
7255 let item_ids_in_pane =
7256 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7257 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7258 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7259 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7260 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7261
7262 // Join into next from left pane into top
7263 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7264 });
7265
7266 workspace.update(cx, |workspace, cx| {
7267 let active_pane = workspace.active_pane();
7268 assert_eq!(top_pane_id, active_pane.entity_id());
7269 assert_eq!(5, active_pane.read(cx).items_len());
7270 let item_ids_in_pane =
7271 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7272 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7273 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7274 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7275 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7276 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7277
7278 // Single pane left: no-op
7279 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7280 });
7281
7282 workspace.update(cx, |workspace, _cx| {
7283 let active_pane = workspace.active_pane();
7284 assert_eq!(top_pane_id, active_pane.entity_id());
7285 });
7286 }
7287
7288 fn add_an_item_to_active_pane(
7289 cx: &mut VisualTestContext,
7290 workspace: &View<Workspace>,
7291 item_id: u64,
7292 ) -> View<TestItem> {
7293 let item = cx.new_view(|cx| {
7294 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7295 item_id,
7296 "item{item_id}.txt",
7297 cx,
7298 )])
7299 });
7300 workspace.update(cx, |workspace, cx| {
7301 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7302 });
7303 return item;
7304 }
7305
7306 fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7307 return workspace.update(cx, |workspace, cx| {
7308 let new_pane =
7309 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7310 new_pane
7311 });
7312 }
7313
7314 #[gpui::test]
7315 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7316 init_test(cx);
7317 let fs = FakeFs::new(cx.executor());
7318 let project = Project::test(fs, None, cx).await;
7319 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7320
7321 add_an_item_to_active_pane(cx, &workspace, 1);
7322 split_pane(cx, &workspace);
7323 add_an_item_to_active_pane(cx, &workspace, 2);
7324 split_pane(cx, &workspace); // empty pane
7325 split_pane(cx, &workspace);
7326 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7327
7328 cx.executor().run_until_parked();
7329
7330 workspace.update(cx, |workspace, cx| {
7331 let num_panes = workspace.panes().len();
7332 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7333 let active_item = workspace
7334 .active_pane()
7335 .read(cx)
7336 .active_item()
7337 .expect("item is in focus");
7338
7339 assert_eq!(num_panes, 4);
7340 assert_eq!(num_items_in_current_pane, 1);
7341 assert_eq!(active_item.item_id(), last_item.item_id());
7342 });
7343
7344 workspace.update(cx, |workspace, cx| {
7345 workspace.join_all_panes(cx);
7346 });
7347
7348 workspace.update(cx, |workspace, cx| {
7349 let num_panes = workspace.panes().len();
7350 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7351 let active_item = workspace
7352 .active_pane()
7353 .read(cx)
7354 .active_item()
7355 .expect("item is in focus");
7356
7357 assert_eq!(num_panes, 1);
7358 assert_eq!(num_items_in_current_pane, 3);
7359 assert_eq!(active_item.item_id(), last_item.item_id());
7360 });
7361 }
7362 struct TestModal(FocusHandle);
7363
7364 impl TestModal {
7365 fn new(cx: &mut ViewContext<Self>) -> Self {
7366 Self(cx.focus_handle())
7367 }
7368 }
7369
7370 impl EventEmitter<DismissEvent> for TestModal {}
7371
7372 impl FocusableView for TestModal {
7373 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7374 self.0.clone()
7375 }
7376 }
7377
7378 impl ModalView for TestModal {}
7379
7380 impl Render for TestModal {
7381 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7382 div().track_focus(&self.0)
7383 }
7384 }
7385
7386 #[gpui::test]
7387 async fn test_panels(cx: &mut gpui::TestAppContext) {
7388 init_test(cx);
7389 let fs = FakeFs::new(cx.executor());
7390
7391 let project = Project::test(fs, [], cx).await;
7392 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7393
7394 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7395 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7396 workspace.add_panel(panel_1.clone(), cx);
7397 workspace.toggle_dock(DockPosition::Left, cx);
7398 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7399 workspace.add_panel(panel_2.clone(), cx);
7400 workspace.toggle_dock(DockPosition::Right, cx);
7401
7402 let left_dock = workspace.left_dock();
7403 assert_eq!(
7404 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7405 panel_1.panel_id()
7406 );
7407 assert_eq!(
7408 left_dock.read(cx).active_panel_size(cx).unwrap(),
7409 panel_1.size(cx)
7410 );
7411
7412 left_dock.update(cx, |left_dock, cx| {
7413 left_dock.resize_active_panel(Some(px(1337.)), cx)
7414 });
7415 assert_eq!(
7416 workspace
7417 .right_dock()
7418 .read(cx)
7419 .visible_panel()
7420 .unwrap()
7421 .panel_id(),
7422 panel_2.panel_id(),
7423 );
7424
7425 (panel_1, panel_2)
7426 });
7427
7428 // Move panel_1 to the right
7429 panel_1.update(cx, |panel_1, cx| {
7430 panel_1.set_position(DockPosition::Right, cx)
7431 });
7432
7433 workspace.update(cx, |workspace, cx| {
7434 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7435 // Since it was the only panel on the left, the left dock should now be closed.
7436 assert!(!workspace.left_dock().read(cx).is_open());
7437 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7438 let right_dock = workspace.right_dock();
7439 assert_eq!(
7440 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7441 panel_1.panel_id()
7442 );
7443 assert_eq!(
7444 right_dock.read(cx).active_panel_size(cx).unwrap(),
7445 px(1337.)
7446 );
7447
7448 // Now we move panel_2 to the left
7449 panel_2.set_position(DockPosition::Left, cx);
7450 });
7451
7452 workspace.update(cx, |workspace, cx| {
7453 // Since panel_2 was not visible on the right, we don't open the left dock.
7454 assert!(!workspace.left_dock().read(cx).is_open());
7455 // And the right dock is unaffected in its displaying of panel_1
7456 assert!(workspace.right_dock().read(cx).is_open());
7457 assert_eq!(
7458 workspace
7459 .right_dock()
7460 .read(cx)
7461 .visible_panel()
7462 .unwrap()
7463 .panel_id(),
7464 panel_1.panel_id(),
7465 );
7466 });
7467
7468 // Move panel_1 back to the left
7469 panel_1.update(cx, |panel_1, cx| {
7470 panel_1.set_position(DockPosition::Left, cx)
7471 });
7472
7473 workspace.update(cx, |workspace, cx| {
7474 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7475 let left_dock = workspace.left_dock();
7476 assert!(left_dock.read(cx).is_open());
7477 assert_eq!(
7478 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7479 panel_1.panel_id()
7480 );
7481 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7482 // And the right dock should be closed as it no longer has any panels.
7483 assert!(!workspace.right_dock().read(cx).is_open());
7484
7485 // Now we move panel_1 to the bottom
7486 panel_1.set_position(DockPosition::Bottom, cx);
7487 });
7488
7489 workspace.update(cx, |workspace, cx| {
7490 // Since panel_1 was visible on the left, we close the left dock.
7491 assert!(!workspace.left_dock().read(cx).is_open());
7492 // The bottom dock is sized based on the panel's default size,
7493 // since the panel orientation changed from vertical to horizontal.
7494 let bottom_dock = workspace.bottom_dock();
7495 assert_eq!(
7496 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7497 panel_1.size(cx),
7498 );
7499 // Close bottom dock and move panel_1 back to the left.
7500 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7501 panel_1.set_position(DockPosition::Left, cx);
7502 });
7503
7504 // Emit activated event on panel 1
7505 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7506
7507 // Now the left dock is open and panel_1 is active and focused.
7508 workspace.update(cx, |workspace, cx| {
7509 let left_dock = workspace.left_dock();
7510 assert!(left_dock.read(cx).is_open());
7511 assert_eq!(
7512 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7513 panel_1.panel_id(),
7514 );
7515 assert!(panel_1.focus_handle(cx).is_focused(cx));
7516 });
7517
7518 // Emit closed event on panel 2, which is not active
7519 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7520
7521 // Wo don't close the left dock, because panel_2 wasn't the active panel
7522 workspace.update(cx, |workspace, cx| {
7523 let left_dock = workspace.left_dock();
7524 assert!(left_dock.read(cx).is_open());
7525 assert_eq!(
7526 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7527 panel_1.panel_id(),
7528 );
7529 });
7530
7531 // Emitting a ZoomIn event shows the panel as zoomed.
7532 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7533 workspace.update(cx, |workspace, _| {
7534 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7535 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7536 });
7537
7538 // Move panel to another dock while it is zoomed
7539 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7540 workspace.update(cx, |workspace, _| {
7541 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7542
7543 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7544 });
7545
7546 // This is a helper for getting a:
7547 // - valid focus on an element,
7548 // - that isn't a part of the panes and panels system of the Workspace,
7549 // - and doesn't trigger the 'on_focus_lost' API.
7550 let focus_other_view = {
7551 let workspace = workspace.clone();
7552 move |cx: &mut VisualTestContext| {
7553 workspace.update(cx, |workspace, cx| {
7554 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7555 workspace.toggle_modal(cx, TestModal::new);
7556 workspace.toggle_modal(cx, TestModal::new);
7557 } else {
7558 workspace.toggle_modal(cx, TestModal::new);
7559 }
7560 })
7561 }
7562 };
7563
7564 // If focus is transferred to another view that's not a panel or another pane, we still show
7565 // the panel as zoomed.
7566 focus_other_view(cx);
7567 workspace.update(cx, |workspace, _| {
7568 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7569 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7570 });
7571
7572 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7573 workspace.update(cx, |_, cx| cx.focus_self());
7574 workspace.update(cx, |workspace, _| {
7575 assert_eq!(workspace.zoomed, None);
7576 assert_eq!(workspace.zoomed_position, None);
7577 });
7578
7579 // If focus is transferred again to another view that's not a panel or a pane, we won't
7580 // show the panel as zoomed because it wasn't zoomed before.
7581 focus_other_view(cx);
7582 workspace.update(cx, |workspace, _| {
7583 assert_eq!(workspace.zoomed, None);
7584 assert_eq!(workspace.zoomed_position, None);
7585 });
7586
7587 // When the panel is activated, it is zoomed again.
7588 cx.dispatch_action(ToggleRightDock);
7589 workspace.update(cx, |workspace, _| {
7590 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7591 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7592 });
7593
7594 // Emitting a ZoomOut event unzooms the panel.
7595 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7596 workspace.update(cx, |workspace, _| {
7597 assert_eq!(workspace.zoomed, None);
7598 assert_eq!(workspace.zoomed_position, None);
7599 });
7600
7601 // Emit closed event on panel 1, which is active
7602 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7603
7604 // Now the left dock is closed, because panel_1 was the active panel
7605 workspace.update(cx, |workspace, cx| {
7606 let right_dock = workspace.right_dock();
7607 assert!(!right_dock.read(cx).is_open());
7608 });
7609 }
7610
7611 #[gpui::test]
7612 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
7613 init_test(cx);
7614
7615 let fs = FakeFs::new(cx.background_executor.clone());
7616 let project = Project::test(fs, [], cx).await;
7617 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7618 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7619
7620 let dirty_regular_buffer = cx.new_view(|cx| {
7621 TestItem::new(cx)
7622 .with_dirty(true)
7623 .with_label("1.txt")
7624 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7625 });
7626 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7627 TestItem::new(cx)
7628 .with_dirty(true)
7629 .with_label("2.txt")
7630 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7631 });
7632 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7633 TestItem::new(cx)
7634 .with_dirty(true)
7635 .with_singleton(false)
7636 .with_label("Fake Project Search")
7637 .with_project_items(&[
7638 dirty_regular_buffer.read(cx).project_items[0].clone(),
7639 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7640 ])
7641 });
7642 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7643 workspace.update(cx, |workspace, cx| {
7644 workspace.add_item(
7645 pane.clone(),
7646 Box::new(dirty_regular_buffer.clone()),
7647 None,
7648 false,
7649 false,
7650 cx,
7651 );
7652 workspace.add_item(
7653 pane.clone(),
7654 Box::new(dirty_regular_buffer_2.clone()),
7655 None,
7656 false,
7657 false,
7658 cx,
7659 );
7660 workspace.add_item(
7661 pane.clone(),
7662 Box::new(dirty_multi_buffer_with_both.clone()),
7663 None,
7664 false,
7665 false,
7666 cx,
7667 );
7668 });
7669
7670 pane.update(cx, |pane, cx| {
7671 pane.activate_item(2, true, true, cx);
7672 assert_eq!(
7673 pane.active_item().unwrap().item_id(),
7674 multi_buffer_with_both_files_id,
7675 "Should select the multi buffer in the pane"
7676 );
7677 });
7678 let close_all_but_multi_buffer_task = pane
7679 .update(cx, |pane, cx| {
7680 pane.close_inactive_items(
7681 &CloseInactiveItems {
7682 save_intent: Some(SaveIntent::Save),
7683 close_pinned: true,
7684 },
7685 cx,
7686 )
7687 })
7688 .expect("should have inactive files to close");
7689 cx.background_executor.run_until_parked();
7690 assert!(
7691 !cx.has_pending_prompt(),
7692 "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
7693 );
7694 close_all_but_multi_buffer_task
7695 .await
7696 .expect("Closing all buffers but the multi buffer failed");
7697 pane.update(cx, |pane, cx| {
7698 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7699 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
7700 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7701 assert_eq!(pane.items_len(), 1);
7702 assert_eq!(
7703 pane.active_item().unwrap().item_id(),
7704 multi_buffer_with_both_files_id,
7705 "Should have only the multi buffer left in the pane"
7706 );
7707 assert!(
7708 dirty_multi_buffer_with_both.read(cx).is_dirty,
7709 "The multi buffer containing the unsaved buffer should still be dirty"
7710 );
7711 });
7712
7713 let close_multi_buffer_task = pane
7714 .update(cx, |pane, cx| {
7715 pane.close_active_item(
7716 &CloseActiveItem {
7717 save_intent: Some(SaveIntent::Close),
7718 },
7719 cx,
7720 )
7721 })
7722 .expect("should have the multi buffer to close");
7723 cx.background_executor.run_until_parked();
7724 assert!(
7725 cx.has_pending_prompt(),
7726 "Dirty multi buffer should prompt a save dialog"
7727 );
7728 cx.simulate_prompt_answer(0);
7729 cx.background_executor.run_until_parked();
7730 close_multi_buffer_task
7731 .await
7732 .expect("Closing the multi buffer failed");
7733 pane.update(cx, |pane, cx| {
7734 assert_eq!(
7735 dirty_multi_buffer_with_both.read(cx).save_count,
7736 1,
7737 "Multi buffer item should get be saved"
7738 );
7739 // Test impl does not save inner items, so we do not assert them
7740 assert_eq!(
7741 pane.items_len(),
7742 0,
7743 "No more items should be left in the pane"
7744 );
7745 assert!(pane.active_item().is_none());
7746 });
7747 }
7748
7749 #[gpui::test]
7750 async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
7751 cx: &mut TestAppContext,
7752 ) {
7753 init_test(cx);
7754
7755 let fs = FakeFs::new(cx.background_executor.clone());
7756 let project = Project::test(fs, [], cx).await;
7757 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7758 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7759
7760 let dirty_regular_buffer = cx.new_view(|cx| {
7761 TestItem::new(cx)
7762 .with_dirty(true)
7763 .with_label("1.txt")
7764 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7765 });
7766 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7767 TestItem::new(cx)
7768 .with_dirty(true)
7769 .with_label("2.txt")
7770 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7771 });
7772 let clear_regular_buffer = cx.new_view(|cx| {
7773 TestItem::new(cx)
7774 .with_label("3.txt")
7775 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7776 });
7777
7778 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7779 TestItem::new(cx)
7780 .with_dirty(true)
7781 .with_singleton(false)
7782 .with_label("Fake Project Search")
7783 .with_project_items(&[
7784 dirty_regular_buffer.read(cx).project_items[0].clone(),
7785 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7786 clear_regular_buffer.read(cx).project_items[0].clone(),
7787 ])
7788 });
7789 workspace.update(cx, |workspace, cx| {
7790 workspace.add_item(
7791 pane.clone(),
7792 Box::new(dirty_regular_buffer.clone()),
7793 None,
7794 false,
7795 false,
7796 cx,
7797 );
7798 workspace.add_item(
7799 pane.clone(),
7800 Box::new(dirty_multi_buffer_with_both.clone()),
7801 None,
7802 false,
7803 false,
7804 cx,
7805 );
7806 });
7807
7808 pane.update(cx, |pane, cx| {
7809 pane.activate_item(0, true, true, cx);
7810 assert_eq!(
7811 pane.active_item().unwrap().item_id(),
7812 dirty_regular_buffer.item_id(),
7813 "Should select the dirty singleton buffer in the pane"
7814 );
7815 });
7816 let close_singleton_buffer_task = pane
7817 .update(cx, |pane, cx| {
7818 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7819 })
7820 .expect("should have active singleton buffer to close");
7821 cx.background_executor.run_until_parked();
7822 assert!(
7823 !cx.has_pending_prompt(),
7824 "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
7825 );
7826
7827 close_singleton_buffer_task
7828 .await
7829 .expect("Should not fail closing the singleton buffer");
7830 pane.update(cx, |pane, cx| {
7831 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7832 assert_eq!(
7833 dirty_multi_buffer_with_both.read(cx).save_count,
7834 0,
7835 "Multi buffer itself should not be saved"
7836 );
7837 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7838 assert_eq!(
7839 pane.items_len(),
7840 1,
7841 "A dirty multi buffer should be present in the pane"
7842 );
7843 assert_eq!(
7844 pane.active_item().unwrap().item_id(),
7845 dirty_multi_buffer_with_both.item_id(),
7846 "Should activate the only remaining item in the pane"
7847 );
7848 });
7849 }
7850
7851 #[gpui::test]
7852 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
7853 cx: &mut TestAppContext,
7854 ) {
7855 init_test(cx);
7856
7857 let fs = FakeFs::new(cx.background_executor.clone());
7858 let project = Project::test(fs, [], cx).await;
7859 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7860 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7861
7862 let dirty_regular_buffer = cx.new_view(|cx| {
7863 TestItem::new(cx)
7864 .with_dirty(true)
7865 .with_label("1.txt")
7866 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7867 });
7868 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7869 TestItem::new(cx)
7870 .with_dirty(true)
7871 .with_label("2.txt")
7872 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7873 });
7874 let clear_regular_buffer = cx.new_view(|cx| {
7875 TestItem::new(cx)
7876 .with_label("3.txt")
7877 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7878 });
7879
7880 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7881 TestItem::new(cx)
7882 .with_dirty(true)
7883 .with_singleton(false)
7884 .with_label("Fake Project Search")
7885 .with_project_items(&[
7886 dirty_regular_buffer.read(cx).project_items[0].clone(),
7887 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7888 clear_regular_buffer.read(cx).project_items[0].clone(),
7889 ])
7890 });
7891 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7892 workspace.update(cx, |workspace, cx| {
7893 workspace.add_item(
7894 pane.clone(),
7895 Box::new(dirty_regular_buffer.clone()),
7896 None,
7897 false,
7898 false,
7899 cx,
7900 );
7901 workspace.add_item(
7902 pane.clone(),
7903 Box::new(dirty_multi_buffer_with_both.clone()),
7904 None,
7905 false,
7906 false,
7907 cx,
7908 );
7909 });
7910
7911 pane.update(cx, |pane, cx| {
7912 pane.activate_item(1, true, true, cx);
7913 assert_eq!(
7914 pane.active_item().unwrap().item_id(),
7915 multi_buffer_with_both_files_id,
7916 "Should select the multi buffer in the pane"
7917 );
7918 });
7919 let _close_multi_buffer_task = pane
7920 .update(cx, |pane, cx| {
7921 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7922 })
7923 .expect("should have active multi buffer to close");
7924 cx.background_executor.run_until_parked();
7925 assert!(
7926 cx.has_pending_prompt(),
7927 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
7928 );
7929 }
7930
7931 #[gpui::test]
7932 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
7933 cx: &mut TestAppContext,
7934 ) {
7935 init_test(cx);
7936
7937 let fs = FakeFs::new(cx.background_executor.clone());
7938 let project = Project::test(fs, [], cx).await;
7939 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7940 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7941
7942 let dirty_regular_buffer = cx.new_view(|cx| {
7943 TestItem::new(cx)
7944 .with_dirty(true)
7945 .with_label("1.txt")
7946 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7947 });
7948 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7949 TestItem::new(cx)
7950 .with_dirty(true)
7951 .with_label("2.txt")
7952 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7953 });
7954 let clear_regular_buffer = cx.new_view(|cx| {
7955 TestItem::new(cx)
7956 .with_label("3.txt")
7957 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7958 });
7959
7960 let dirty_multi_buffer = cx.new_view(|cx| {
7961 TestItem::new(cx)
7962 .with_dirty(true)
7963 .with_singleton(false)
7964 .with_label("Fake Project Search")
7965 .with_project_items(&[
7966 dirty_regular_buffer.read(cx).project_items[0].clone(),
7967 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7968 clear_regular_buffer.read(cx).project_items[0].clone(),
7969 ])
7970 });
7971 workspace.update(cx, |workspace, cx| {
7972 workspace.add_item(
7973 pane.clone(),
7974 Box::new(dirty_regular_buffer.clone()),
7975 None,
7976 false,
7977 false,
7978 cx,
7979 );
7980 workspace.add_item(
7981 pane.clone(),
7982 Box::new(dirty_regular_buffer_2.clone()),
7983 None,
7984 false,
7985 false,
7986 cx,
7987 );
7988 workspace.add_item(
7989 pane.clone(),
7990 Box::new(dirty_multi_buffer.clone()),
7991 None,
7992 false,
7993 false,
7994 cx,
7995 );
7996 });
7997
7998 pane.update(cx, |pane, cx| {
7999 pane.activate_item(2, true, true, cx);
8000 assert_eq!(
8001 pane.active_item().unwrap().item_id(),
8002 dirty_multi_buffer.item_id(),
8003 "Should select the multi buffer in the pane"
8004 );
8005 });
8006 let close_multi_buffer_task = pane
8007 .update(cx, |pane, cx| {
8008 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
8009 })
8010 .expect("should have active multi buffer to close");
8011 cx.background_executor.run_until_parked();
8012 assert!(
8013 !cx.has_pending_prompt(),
8014 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8015 );
8016 close_multi_buffer_task
8017 .await
8018 .expect("Closing multi buffer failed");
8019 pane.update(cx, |pane, cx| {
8020 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8021 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8022 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8023 assert_eq!(
8024 pane.items()
8025 .map(|item| item.item_id())
8026 .sorted()
8027 .collect::<Vec<_>>(),
8028 vec![
8029 dirty_regular_buffer.item_id(),
8030 dirty_regular_buffer_2.item_id(),
8031 ],
8032 "Should have no multi buffer left in the pane"
8033 );
8034 assert!(dirty_regular_buffer.read(cx).is_dirty);
8035 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8036 });
8037 }
8038
8039 #[gpui::test]
8040 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8041 init_test(cx);
8042 let fs = FakeFs::new(cx.executor());
8043 let project = Project::test(fs, [], cx).await;
8044 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
8045
8046 // Add a new panel to the right dock, opening the dock and setting the
8047 // focus to the new panel.
8048 let panel = workspace.update(cx, |workspace, cx| {
8049 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
8050 workspace.add_panel(panel.clone(), cx);
8051
8052 workspace
8053 .right_dock()
8054 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
8055
8056 workspace.toggle_panel_focus::<TestPanel>(cx);
8057
8058 panel
8059 });
8060
8061 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8062 // panel to the next valid position which, in this case, is the left
8063 // dock.
8064 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8065 workspace.update(cx, |workspace, cx| {
8066 assert!(workspace.left_dock().read(cx).is_open());
8067 assert_eq!(panel.read(cx).position, DockPosition::Left);
8068 });
8069
8070 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8071 // panel to the next valid position which, in this case, is the bottom
8072 // dock.
8073 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8074 workspace.update(cx, |workspace, cx| {
8075 assert!(workspace.bottom_dock().read(cx).is_open());
8076 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8077 });
8078
8079 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8080 // around moving the panel to its initial position, the right dock.
8081 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8082 workspace.update(cx, |workspace, cx| {
8083 assert!(workspace.right_dock().read(cx).is_open());
8084 assert_eq!(panel.read(cx).position, DockPosition::Right);
8085 });
8086
8087 // Remove focus from the panel, ensuring that, if the panel is not
8088 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8089 // the panel's position, so the panel is still in the right dock.
8090 workspace.update(cx, |workspace, cx| {
8091 workspace.toggle_panel_focus::<TestPanel>(cx);
8092 });
8093
8094 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8095 workspace.update(cx, |workspace, cx| {
8096 assert!(workspace.right_dock().read(cx).is_open());
8097 assert_eq!(panel.read(cx).position, DockPosition::Right);
8098 });
8099 }
8100
8101 mod register_project_item_tests {
8102 use gpui::Context as _;
8103
8104 use super::*;
8105
8106 // View
8107 struct TestPngItemView {
8108 focus_handle: FocusHandle,
8109 }
8110 // Model
8111 struct TestPngItem {}
8112
8113 impl project::ProjectItem for TestPngItem {
8114 fn try_open(
8115 _project: &Model<Project>,
8116 path: &ProjectPath,
8117 cx: &mut AppContext,
8118 ) -> Option<Task<gpui::Result<Model<Self>>>> {
8119 if path.path.extension().unwrap() == "png" {
8120 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
8121 } else {
8122 None
8123 }
8124 }
8125
8126 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
8127 None
8128 }
8129
8130 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
8131 None
8132 }
8133
8134 fn is_dirty(&self) -> bool {
8135 false
8136 }
8137 }
8138
8139 impl Item for TestPngItemView {
8140 type Event = ();
8141 }
8142 impl EventEmitter<()> for TestPngItemView {}
8143 impl FocusableView for TestPngItemView {
8144 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8145 self.focus_handle.clone()
8146 }
8147 }
8148
8149 impl Render for TestPngItemView {
8150 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8151 Empty
8152 }
8153 }
8154
8155 impl ProjectItem for TestPngItemView {
8156 type Item = TestPngItem;
8157
8158 fn for_project_item(
8159 _project: Model<Project>,
8160 _item: Model<Self::Item>,
8161 cx: &mut ViewContext<Self>,
8162 ) -> Self
8163 where
8164 Self: Sized,
8165 {
8166 Self {
8167 focus_handle: cx.focus_handle(),
8168 }
8169 }
8170 }
8171
8172 // View
8173 struct TestIpynbItemView {
8174 focus_handle: FocusHandle,
8175 }
8176 // Model
8177 struct TestIpynbItem {}
8178
8179 impl project::ProjectItem for TestIpynbItem {
8180 fn try_open(
8181 _project: &Model<Project>,
8182 path: &ProjectPath,
8183 cx: &mut AppContext,
8184 ) -> Option<Task<gpui::Result<Model<Self>>>> {
8185 if path.path.extension().unwrap() == "ipynb" {
8186 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
8187 } else {
8188 None
8189 }
8190 }
8191
8192 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
8193 None
8194 }
8195
8196 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
8197 None
8198 }
8199
8200 fn is_dirty(&self) -> bool {
8201 false
8202 }
8203 }
8204
8205 impl Item for TestIpynbItemView {
8206 type Event = ();
8207 }
8208 impl EventEmitter<()> for TestIpynbItemView {}
8209 impl FocusableView for TestIpynbItemView {
8210 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8211 self.focus_handle.clone()
8212 }
8213 }
8214
8215 impl Render for TestIpynbItemView {
8216 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8217 Empty
8218 }
8219 }
8220
8221 impl ProjectItem for TestIpynbItemView {
8222 type Item = TestIpynbItem;
8223
8224 fn for_project_item(
8225 _project: Model<Project>,
8226 _item: Model<Self::Item>,
8227 cx: &mut ViewContext<Self>,
8228 ) -> Self
8229 where
8230 Self: Sized,
8231 {
8232 Self {
8233 focus_handle: cx.focus_handle(),
8234 }
8235 }
8236 }
8237
8238 struct TestAlternatePngItemView {
8239 focus_handle: FocusHandle,
8240 }
8241
8242 impl Item for TestAlternatePngItemView {
8243 type Event = ();
8244 }
8245
8246 impl EventEmitter<()> for TestAlternatePngItemView {}
8247 impl FocusableView for TestAlternatePngItemView {
8248 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8249 self.focus_handle.clone()
8250 }
8251 }
8252
8253 impl Render for TestAlternatePngItemView {
8254 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8255 Empty
8256 }
8257 }
8258
8259 impl ProjectItem for TestAlternatePngItemView {
8260 type Item = TestPngItem;
8261
8262 fn for_project_item(
8263 _project: Model<Project>,
8264 _item: Model<Self::Item>,
8265 cx: &mut ViewContext<Self>,
8266 ) -> Self
8267 where
8268 Self: Sized,
8269 {
8270 Self {
8271 focus_handle: cx.focus_handle(),
8272 }
8273 }
8274 }
8275
8276 #[gpui::test]
8277 async fn test_register_project_item(cx: &mut TestAppContext) {
8278 init_test(cx);
8279
8280 cx.update(|cx| {
8281 register_project_item::<TestPngItemView>(cx);
8282 register_project_item::<TestIpynbItemView>(cx);
8283 });
8284
8285 let fs = FakeFs::new(cx.executor());
8286 fs.insert_tree(
8287 "/root1",
8288 json!({
8289 "one.png": "BINARYDATAHERE",
8290 "two.ipynb": "{ totally a notebook }",
8291 "three.txt": "editing text, sure why not?"
8292 }),
8293 )
8294 .await;
8295
8296 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8297 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8298
8299 let worktree_id = project.update(cx, |project, cx| {
8300 project.worktrees(cx).next().unwrap().read(cx).id()
8301 });
8302
8303 let handle = workspace
8304 .update(cx, |workspace, cx| {
8305 let project_path = (worktree_id, "one.png");
8306 workspace.open_path(project_path, None, true, cx)
8307 })
8308 .await
8309 .unwrap();
8310
8311 // Now we can check if the handle we got back errored or not
8312 assert_eq!(
8313 handle.to_any().entity_type(),
8314 TypeId::of::<TestPngItemView>()
8315 );
8316
8317 let handle = workspace
8318 .update(cx, |workspace, cx| {
8319 let project_path = (worktree_id, "two.ipynb");
8320 workspace.open_path(project_path, None, true, cx)
8321 })
8322 .await
8323 .unwrap();
8324
8325 assert_eq!(
8326 handle.to_any().entity_type(),
8327 TypeId::of::<TestIpynbItemView>()
8328 );
8329
8330 let handle = workspace
8331 .update(cx, |workspace, cx| {
8332 let project_path = (worktree_id, "three.txt");
8333 workspace.open_path(project_path, None, true, cx)
8334 })
8335 .await;
8336 assert!(handle.is_err());
8337 }
8338
8339 #[gpui::test]
8340 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8341 init_test(cx);
8342
8343 cx.update(|cx| {
8344 register_project_item::<TestPngItemView>(cx);
8345 register_project_item::<TestAlternatePngItemView>(cx);
8346 });
8347
8348 let fs = FakeFs::new(cx.executor());
8349 fs.insert_tree(
8350 "/root1",
8351 json!({
8352 "one.png": "BINARYDATAHERE",
8353 "two.ipynb": "{ totally a notebook }",
8354 "three.txt": "editing text, sure why not?"
8355 }),
8356 )
8357 .await;
8358
8359 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8360 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8361
8362 let worktree_id = project.update(cx, |project, cx| {
8363 project.worktrees(cx).next().unwrap().read(cx).id()
8364 });
8365
8366 let handle = workspace
8367 .update(cx, |workspace, cx| {
8368 let project_path = (worktree_id, "one.png");
8369 workspace.open_path(project_path, None, true, cx)
8370 })
8371 .await
8372 .unwrap();
8373
8374 // This _must_ be the second item registered
8375 assert_eq!(
8376 handle.to_any().entity_type(),
8377 TypeId::of::<TestAlternatePngItemView>()
8378 );
8379
8380 let handle = workspace
8381 .update(cx, |workspace, cx| {
8382 let project_path = (worktree_id, "three.txt");
8383 workspace.open_path(project_path, None, true, cx)
8384 })
8385 .await;
8386 assert!(handle.is_err());
8387 }
8388 }
8389
8390 pub fn init_test(cx: &mut TestAppContext) {
8391 cx.update(|cx| {
8392 let settings_store = SettingsStore::test(cx);
8393 cx.set_global(settings_store);
8394 theme::init(theme::LoadThemes::JustBase, cx);
8395 language::init(cx);
8396 crate::init_settings(cx);
8397 Project::init_settings(cx);
8398 });
8399 }
8400
8401 fn dirty_project_item(id: u64, path: &str, cx: &mut AppContext) -> Model<TestProjectItem> {
8402 let item = TestProjectItem::new(id, path, cx);
8403 item.update(cx, |item, _| {
8404 item.is_dirty = true;
8405 });
8406 item
8407 }
8408}