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