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