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