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