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