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