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