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 fn shared_screen_for_peer(
3948 &self,
3949 peer_id: PeerId,
3950 pane: &View<Pane>,
3951 cx: &mut WindowContext,
3952 ) -> Option<View<SharedScreen>> {
3953 let call = self.active_call()?;
3954 let room = call.read(cx).room()?.read(cx);
3955 let participant = room.remote_participant_for_peer_id(peer_id)?;
3956 let track = participant.video_tracks.values().next()?.clone();
3957 let user = participant.user.clone();
3958
3959 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3960 if item.read(cx).peer_id == peer_id {
3961 return Some(item);
3962 }
3963 }
3964
3965 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3966 }
3967
3968 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3969 if cx.is_window_active() {
3970 self.update_active_view_for_followers(cx);
3971
3972 if let Some(database_id) = self.database_id {
3973 cx.background_executor()
3974 .spawn(persistence::DB.update_timestamp(database_id))
3975 .detach();
3976 }
3977 } else {
3978 for pane in &self.panes {
3979 pane.update(cx, |pane, cx| {
3980 if let Some(item) = pane.active_item() {
3981 item.workspace_deactivated(cx);
3982 }
3983 for item in pane.items() {
3984 if matches!(
3985 item.workspace_settings(cx).autosave,
3986 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3987 ) {
3988 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3989 .detach_and_log_err(cx);
3990 }
3991 }
3992 });
3993 }
3994 }
3995 }
3996
3997 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3998 self.active_call.as_ref().map(|(call, _)| call)
3999 }
4000
4001 fn on_active_call_event(
4002 &mut self,
4003 _: Model<ActiveCall>,
4004 event: &call::room::Event,
4005 cx: &mut ViewContext<Self>,
4006 ) {
4007 match event {
4008 call::room::Event::ParticipantLocationChanged { participant_id }
4009 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4010 self.leader_updated(*participant_id, cx);
4011 }
4012 _ => {}
4013 }
4014 }
4015
4016 pub fn database_id(&self) -> Option<WorkspaceId> {
4017 self.database_id
4018 }
4019
4020 fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
4021 let project = self.project().read(cx);
4022
4023 if project.is_local() {
4024 Some(
4025 project
4026 .visible_worktrees(cx)
4027 .map(|worktree| worktree.read(cx).abs_path())
4028 .collect::<Vec<_>>(),
4029 )
4030 } else {
4031 None
4032 }
4033 }
4034
4035 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
4036 match member {
4037 Member::Axis(PaneAxis { members, .. }) => {
4038 for child in members.iter() {
4039 self.remove_panes(child.clone(), cx)
4040 }
4041 }
4042 Member::Pane(pane) => {
4043 self.force_remove_pane(&pane, &None, cx);
4044 }
4045 }
4046 }
4047
4048 fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
4049 self.session_id.take();
4050 self.serialize_workspace_internal(cx)
4051 }
4052
4053 fn force_remove_pane(
4054 &mut self,
4055 pane: &View<Pane>,
4056 focus_on: &Option<View<Pane>>,
4057 cx: &mut ViewContext<Workspace>,
4058 ) {
4059 self.panes.retain(|p| p != pane);
4060 if let Some(focus_on) = focus_on {
4061 focus_on.update(cx, |pane, cx| pane.focus(cx));
4062 } else {
4063 self.panes
4064 .last()
4065 .unwrap()
4066 .update(cx, |pane, cx| pane.focus(cx));
4067 }
4068 if self.last_active_center_pane == Some(pane.downgrade()) {
4069 self.last_active_center_pane = None;
4070 }
4071 cx.notify();
4072 }
4073
4074 fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4075 if self._schedule_serialize.is_none() {
4076 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4077 cx.background_executor()
4078 .timer(Duration::from_millis(100))
4079 .await;
4080 this.update(&mut cx, |this, cx| {
4081 this.serialize_workspace_internal(cx).detach();
4082 this._schedule_serialize.take();
4083 })
4084 .log_err();
4085 }));
4086 }
4087 }
4088
4089 fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4090 let Some(database_id) = self.database_id() else {
4091 return Task::ready(());
4092 };
4093
4094 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4095 let (items, active, pinned_count) = {
4096 let pane = pane_handle.read(cx);
4097 let active_item_id = pane.active_item().map(|item| item.item_id());
4098 (
4099 pane.items()
4100 .filter_map(|handle| {
4101 let handle = handle.to_serializable_item_handle(cx)?;
4102
4103 Some(SerializedItem {
4104 kind: Arc::from(handle.serialized_item_kind()),
4105 item_id: handle.item_id().as_u64(),
4106 active: Some(handle.item_id()) == active_item_id,
4107 preview: pane.is_active_preview_item(handle.item_id()),
4108 })
4109 })
4110 .collect::<Vec<_>>(),
4111 pane.has_focus(cx),
4112 pane.pinned_count(),
4113 )
4114 };
4115
4116 SerializedPane::new(items, active, pinned_count)
4117 }
4118
4119 fn build_serialized_pane_group(
4120 pane_group: &Member,
4121 cx: &WindowContext,
4122 ) -> SerializedPaneGroup {
4123 match pane_group {
4124 Member::Axis(PaneAxis {
4125 axis,
4126 members,
4127 flexes,
4128 bounding_boxes: _,
4129 }) => SerializedPaneGroup::Group {
4130 axis: SerializedAxis(*axis),
4131 children: members
4132 .iter()
4133 .map(|member| build_serialized_pane_group(member, cx))
4134 .collect::<Vec<_>>(),
4135 flexes: Some(flexes.lock().clone()),
4136 },
4137 Member::Pane(pane_handle) => {
4138 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4139 }
4140 }
4141 }
4142
4143 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4144 let left_dock = this.left_dock.read(cx);
4145 let left_visible = left_dock.is_open();
4146 let left_active_panel = left_dock
4147 .active_panel()
4148 .map(|panel| panel.persistent_name().to_string());
4149 let left_dock_zoom = left_dock
4150 .active_panel()
4151 .map(|panel| panel.is_zoomed(cx))
4152 .unwrap_or(false);
4153
4154 let right_dock = this.right_dock.read(cx);
4155 let right_visible = right_dock.is_open();
4156 let right_active_panel = right_dock
4157 .active_panel()
4158 .map(|panel| panel.persistent_name().to_string());
4159 let right_dock_zoom = right_dock
4160 .active_panel()
4161 .map(|panel| panel.is_zoomed(cx))
4162 .unwrap_or(false);
4163
4164 let bottom_dock = this.bottom_dock.read(cx);
4165 let bottom_visible = bottom_dock.is_open();
4166 let bottom_active_panel = bottom_dock
4167 .active_panel()
4168 .map(|panel| panel.persistent_name().to_string());
4169 let bottom_dock_zoom = bottom_dock
4170 .active_panel()
4171 .map(|panel| panel.is_zoomed(cx))
4172 .unwrap_or(false);
4173
4174 DockStructure {
4175 left: DockData {
4176 visible: left_visible,
4177 active_panel: left_active_panel,
4178 zoom: left_dock_zoom,
4179 },
4180 right: DockData {
4181 visible: right_visible,
4182 active_panel: right_active_panel,
4183 zoom: right_dock_zoom,
4184 },
4185 bottom: DockData {
4186 visible: bottom_visible,
4187 active_panel: bottom_active_panel,
4188 zoom: bottom_dock_zoom,
4189 },
4190 }
4191 }
4192
4193 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4194 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4195 } else if let Some(local_paths) = self.local_paths(cx) {
4196 if !local_paths.is_empty() {
4197 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4198 } else {
4199 None
4200 }
4201 } else {
4202 None
4203 };
4204
4205 if let Some(location) = location {
4206 let center_group = build_serialized_pane_group(&self.center.root, cx);
4207 let docks = build_serialized_docks(self, cx);
4208 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4209 let serialized_workspace = SerializedWorkspace {
4210 id: database_id,
4211 location,
4212 center_group,
4213 window_bounds,
4214 display: Default::default(),
4215 docks,
4216 centered_layout: self.centered_layout,
4217 session_id: self.session_id.clone(),
4218 window_id: Some(cx.window_handle().window_id().as_u64()),
4219 };
4220 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4221 }
4222 Task::ready(())
4223 }
4224
4225 async fn serialize_items(
4226 this: &WeakView<Self>,
4227 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4228 cx: &mut AsyncWindowContext,
4229 ) -> Result<()> {
4230 const CHUNK_SIZE: usize = 200;
4231 const THROTTLE_TIME: Duration = Duration::from_millis(200);
4232
4233 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4234
4235 while let Some(items_received) = serializable_items.next().await {
4236 let unique_items =
4237 items_received
4238 .into_iter()
4239 .fold(HashMap::default(), |mut acc, item| {
4240 acc.entry(item.item_id()).or_insert(item);
4241 acc
4242 });
4243
4244 // We use into_iter() here so that the references to the items are moved into
4245 // the tasks and not kept alive while we're sleeping.
4246 for (_, item) in unique_items.into_iter() {
4247 if let Ok(Some(task)) =
4248 this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4249 {
4250 cx.background_executor()
4251 .spawn(async move { task.await.log_err() })
4252 .detach();
4253 }
4254 }
4255
4256 cx.background_executor().timer(THROTTLE_TIME).await;
4257 }
4258
4259 Ok(())
4260 }
4261
4262 pub(crate) fn enqueue_item_serialization(
4263 &mut self,
4264 item: Box<dyn SerializableItemHandle>,
4265 ) -> Result<()> {
4266 self.serializable_items_tx
4267 .unbounded_send(item)
4268 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4269 }
4270
4271 pub(crate) fn load_workspace(
4272 serialized_workspace: SerializedWorkspace,
4273 paths_to_open: Vec<Option<ProjectPath>>,
4274 cx: &mut ViewContext<Workspace>,
4275 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4276 cx.spawn(|workspace, mut cx| async move {
4277 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4278
4279 let mut center_group = None;
4280 let mut center_items = None;
4281
4282 // Traverse the splits tree and add to things
4283 if let Some((group, active_pane, items)) = serialized_workspace
4284 .center_group
4285 .deserialize(
4286 &project,
4287 serialized_workspace.id,
4288 workspace.clone(),
4289 &mut cx,
4290 )
4291 .await
4292 {
4293 center_items = Some(items);
4294 center_group = Some((group, active_pane))
4295 }
4296
4297 let mut items_by_project_path = HashMap::default();
4298 let mut item_ids_by_kind = HashMap::default();
4299 let mut all_deserialized_items = Vec::default();
4300 cx.update(|cx| {
4301 for item in center_items.unwrap_or_default().into_iter().flatten() {
4302 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4303 item_ids_by_kind
4304 .entry(serializable_item_handle.serialized_item_kind())
4305 .or_insert(Vec::new())
4306 .push(item.item_id().as_u64() as ItemId);
4307 }
4308
4309 if let Some(project_path) = item.project_path(cx) {
4310 items_by_project_path.insert(project_path, item.clone());
4311 }
4312 all_deserialized_items.push(item);
4313 }
4314 })?;
4315
4316 let opened_items = paths_to_open
4317 .into_iter()
4318 .map(|path_to_open| {
4319 path_to_open
4320 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4321 })
4322 .collect::<Vec<_>>();
4323
4324 // Remove old panes from workspace panes list
4325 workspace.update(&mut cx, |workspace, cx| {
4326 if let Some((center_group, active_pane)) = center_group {
4327 workspace.remove_panes(workspace.center.root.clone(), cx);
4328
4329 // Swap workspace center group
4330 workspace.center = PaneGroup::with_root(center_group);
4331 if let Some(active_pane) = active_pane {
4332 workspace.set_active_pane(&active_pane, cx);
4333 cx.focus_self();
4334 } else {
4335 workspace.set_active_pane(&workspace.center.first_pane(), cx);
4336 }
4337 }
4338
4339 let docks = serialized_workspace.docks;
4340
4341 for (dock, serialized_dock) in [
4342 (&mut workspace.right_dock, docks.right),
4343 (&mut workspace.left_dock, docks.left),
4344 (&mut workspace.bottom_dock, docks.bottom),
4345 ]
4346 .iter_mut()
4347 {
4348 dock.update(cx, |dock, cx| {
4349 dock.serialized_dock = Some(serialized_dock.clone());
4350 dock.restore_state(cx);
4351 });
4352 }
4353
4354 cx.notify();
4355 })?;
4356
4357 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4358 // after loading the items, we might have different items and in order to avoid
4359 // the database filling up, we delete items that haven't been loaded now.
4360 //
4361 // The items that have been loaded, have been saved after they've been added to the workspace.
4362 let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4363 item_ids_by_kind
4364 .into_iter()
4365 .map(|(item_kind, loaded_items)| {
4366 SerializableItemRegistry::cleanup(
4367 item_kind,
4368 serialized_workspace.id,
4369 loaded_items,
4370 cx,
4371 )
4372 .log_err()
4373 })
4374 .collect::<Vec<_>>()
4375 })?;
4376
4377 futures::future::join_all(clean_up_tasks).await;
4378
4379 workspace
4380 .update(&mut cx, |workspace, cx| {
4381 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4382 workspace.serialize_workspace_internal(cx).detach();
4383
4384 // Ensure that we mark the window as edited if we did load dirty items
4385 workspace.update_window_edited(cx);
4386 })
4387 .ok();
4388
4389 Ok(opened_items)
4390 })
4391 }
4392
4393 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4394 self.add_workspace_actions_listeners(div, cx)
4395 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4396 .on_action(cx.listener(Self::close_all_items_and_panes))
4397 .on_action(cx.listener(Self::save_all))
4398 .on_action(cx.listener(Self::send_keystrokes))
4399 .on_action(cx.listener(Self::add_folder_to_project))
4400 .on_action(cx.listener(Self::follow_next_collaborator))
4401 .on_action(cx.listener(Self::close_window))
4402 .on_action(cx.listener(Self::activate_pane_at_index))
4403 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4404 let pane = workspace.active_pane().clone();
4405 workspace.unfollow_in_pane(&pane, cx);
4406 }))
4407 .on_action(cx.listener(|workspace, action: &Save, cx| {
4408 workspace
4409 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4410 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4411 }))
4412 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4413 workspace
4414 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4415 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4416 }))
4417 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4418 workspace
4419 .save_active_item(SaveIntent::SaveAs, cx)
4420 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4421 }))
4422 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4423 workspace.activate_previous_pane(cx)
4424 }))
4425 .on_action(
4426 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4427 )
4428 .on_action(
4429 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4430 workspace.activate_pane_in_direction(action.0, cx)
4431 }),
4432 )
4433 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4434 workspace.swap_pane_in_direction(action.0, cx)
4435 }))
4436 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4437 this.toggle_dock(DockPosition::Left, cx);
4438 }))
4439 .on_action(
4440 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4441 workspace.toggle_dock(DockPosition::Right, cx);
4442 }),
4443 )
4444 .on_action(
4445 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4446 workspace.toggle_dock(DockPosition::Bottom, cx);
4447 }),
4448 )
4449 .on_action(
4450 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4451 workspace.close_all_docks(cx);
4452 }),
4453 )
4454 .on_action(
4455 cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4456 workspace.clear_all_notifications(cx);
4457 }),
4458 )
4459 .on_action(
4460 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4461 workspace.reopen_closed_item(cx).detach();
4462 }),
4463 )
4464 .on_action(cx.listener(Workspace::toggle_centered_layout))
4465 }
4466
4467 #[cfg(any(test, feature = "test-support"))]
4468 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4469 use node_runtime::NodeRuntime;
4470 use session::Session;
4471
4472 let client = project.read(cx).client();
4473 let user_store = project.read(cx).user_store();
4474
4475 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4476 let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4477 cx.activate_window();
4478 let app_state = Arc::new(AppState {
4479 languages: project.read(cx).languages().clone(),
4480 workspace_store,
4481 client,
4482 user_store,
4483 fs: project.read(cx).fs().clone(),
4484 build_window_options: |_, _| Default::default(),
4485 node_runtime: NodeRuntime::unavailable(),
4486 session,
4487 });
4488 let workspace = Self::new(Default::default(), project, app_state, cx);
4489 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4490 workspace
4491 }
4492
4493 pub fn register_action<A: Action>(
4494 &mut self,
4495 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4496 ) -> &mut Self {
4497 let callback = Arc::new(callback);
4498
4499 self.workspace_actions.push(Box::new(move |div, cx| {
4500 let callback = callback.clone();
4501 div.on_action(
4502 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4503 )
4504 }));
4505 self
4506 }
4507
4508 fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4509 for action in self.workspace_actions.iter() {
4510 div = (action)(div, cx)
4511 }
4512 div
4513 }
4514
4515 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
4516 self.modal_layer.read(cx).has_active_modal()
4517 }
4518
4519 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
4520 self.modal_layer.read(cx).active_modal()
4521 }
4522
4523 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4524 where
4525 B: FnOnce(&mut ViewContext<V>) -> V,
4526 {
4527 self.modal_layer
4528 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4529 }
4530
4531 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4532 self.centered_layout = !self.centered_layout;
4533 if let Some(database_id) = self.database_id() {
4534 cx.background_executor()
4535 .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4536 .detach_and_log_err(cx);
4537 }
4538 cx.notify();
4539 }
4540
4541 fn adjust_padding(padding: Option<f32>) -> f32 {
4542 padding
4543 .unwrap_or(Self::DEFAULT_PADDING)
4544 .clamp(0.0, Self::MAX_PADDING)
4545 }
4546
4547 fn render_dock(
4548 &self,
4549 position: DockPosition,
4550 dock: &View<Dock>,
4551 cx: &WindowContext,
4552 ) -> Option<Div> {
4553 if self.zoomed_position == Some(position) {
4554 return None;
4555 }
4556
4557 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4558 let pane = panel.pane(cx)?;
4559 let follower_states = &self.follower_states;
4560 leader_border_for_pane(follower_states, &pane, cx)
4561 });
4562
4563 Some(
4564 div()
4565 .flex()
4566 .flex_none()
4567 .overflow_hidden()
4568 .child(dock.clone())
4569 .children(leader_border),
4570 )
4571 }
4572
4573 pub fn for_window(cx: &mut WindowContext) -> Option<View<Workspace>> {
4574 let window = cx.window_handle().downcast::<Workspace>()?;
4575 cx.read_window(&window, |workspace, _| workspace).ok()
4576 }
4577
4578 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
4579 self.zoomed.as_ref()
4580 }
4581}
4582
4583fn leader_border_for_pane(
4584 follower_states: &HashMap<PeerId, FollowerState>,
4585 pane: &View<Pane>,
4586 cx: &WindowContext,
4587) -> Option<Div> {
4588 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4589 if state.pane() == pane {
4590 Some((*leader_id, state))
4591 } else {
4592 None
4593 }
4594 })?;
4595
4596 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4597 let leader = room.remote_participant_for_peer_id(leader_id)?;
4598
4599 let mut leader_color = cx
4600 .theme()
4601 .players()
4602 .color_for_participant(leader.participant_index.0)
4603 .cursor;
4604 leader_color.fade_out(0.3);
4605 Some(
4606 div()
4607 .absolute()
4608 .size_full()
4609 .left_0()
4610 .top_0()
4611 .border_2()
4612 .border_color(leader_color),
4613 )
4614}
4615
4616fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4617 ZED_WINDOW_POSITION
4618 .zip(*ZED_WINDOW_SIZE)
4619 .map(|(position, size)| Bounds {
4620 origin: position,
4621 size,
4622 })
4623}
4624
4625fn open_items(
4626 serialized_workspace: Option<SerializedWorkspace>,
4627 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4628 cx: &mut ViewContext<Workspace>,
4629) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4630 let restored_items = serialized_workspace.map(|serialized_workspace| {
4631 Workspace::load_workspace(
4632 serialized_workspace,
4633 project_paths_to_open
4634 .iter()
4635 .map(|(_, project_path)| project_path)
4636 .cloned()
4637 .collect(),
4638 cx,
4639 )
4640 });
4641
4642 cx.spawn(|workspace, mut cx| async move {
4643 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4644
4645 if let Some(restored_items) = restored_items {
4646 let restored_items = restored_items.await?;
4647
4648 let restored_project_paths = restored_items
4649 .iter()
4650 .filter_map(|item| {
4651 cx.update(|cx| item.as_ref()?.project_path(cx))
4652 .ok()
4653 .flatten()
4654 })
4655 .collect::<HashSet<_>>();
4656
4657 for restored_item in restored_items {
4658 opened_items.push(restored_item.map(Ok));
4659 }
4660
4661 project_paths_to_open
4662 .iter_mut()
4663 .for_each(|(_, project_path)| {
4664 if let Some(project_path_to_open) = project_path {
4665 if restored_project_paths.contains(project_path_to_open) {
4666 *project_path = None;
4667 }
4668 }
4669 });
4670 } else {
4671 for _ in 0..project_paths_to_open.len() {
4672 opened_items.push(None);
4673 }
4674 }
4675 assert!(opened_items.len() == project_paths_to_open.len());
4676
4677 let tasks =
4678 project_paths_to_open
4679 .into_iter()
4680 .enumerate()
4681 .map(|(ix, (abs_path, project_path))| {
4682 let workspace = workspace.clone();
4683 cx.spawn(|mut cx| async move {
4684 let file_project_path = project_path?;
4685 let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4686 workspace.project().update(cx, |project, cx| {
4687 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4688 })
4689 });
4690
4691 // We only want to open file paths here. If one of the items
4692 // here is a directory, it was already opened further above
4693 // with a `find_or_create_worktree`.
4694 if let Ok(task) = abs_path_task {
4695 if task.await.map_or(true, |p| p.is_file()) {
4696 return Some((
4697 ix,
4698 workspace
4699 .update(&mut cx, |workspace, cx| {
4700 workspace.open_path(file_project_path, None, true, cx)
4701 })
4702 .log_err()?
4703 .await,
4704 ));
4705 }
4706 }
4707 None
4708 })
4709 });
4710
4711 let tasks = tasks.collect::<Vec<_>>();
4712
4713 let tasks = futures::future::join_all(tasks);
4714 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4715 opened_items[ix] = Some(path_open_result);
4716 }
4717
4718 Ok(opened_items)
4719 })
4720}
4721
4722enum ActivateInDirectionTarget {
4723 Pane(View<Pane>),
4724 Dock(View<Dock>),
4725}
4726
4727fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4728 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";
4729
4730 workspace
4731 .update(cx, |workspace, cx| {
4732 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4733 struct DatabaseFailedNotification;
4734
4735 workspace.show_notification_once(
4736 NotificationId::unique::<DatabaseFailedNotification>(),
4737 cx,
4738 |cx| {
4739 cx.new_view(|_| {
4740 MessageNotification::new("Failed to load the database file.")
4741 .with_click_message("File an issue")
4742 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4743 })
4744 },
4745 );
4746 }
4747 })
4748 .log_err();
4749}
4750
4751impl FocusableView for Workspace {
4752 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4753 self.active_pane.focus_handle(cx)
4754 }
4755}
4756
4757#[derive(Clone, Render)]
4758struct DraggedDock(DockPosition);
4759
4760impl Render for Workspace {
4761 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4762 let mut context = KeyContext::new_with_defaults();
4763 context.add("Workspace");
4764 context.set("keyboard_layout", cx.keyboard_layout().clone());
4765 let centered_layout = self.centered_layout
4766 && self.center.panes().len() == 1
4767 && self.active_item(cx).is_some();
4768 let render_padding = |size| {
4769 (size > 0.0).then(|| {
4770 div()
4771 .h_full()
4772 .w(relative(size))
4773 .bg(cx.theme().colors().editor_background)
4774 .border_color(cx.theme().colors().pane_group_border)
4775 })
4776 };
4777 let paddings = if centered_layout {
4778 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4779 (
4780 render_padding(Self::adjust_padding(settings.left_padding)),
4781 render_padding(Self::adjust_padding(settings.right_padding)),
4782 )
4783 } else {
4784 (None, None)
4785 };
4786 let ui_font = theme::setup_ui_font(cx);
4787
4788 let theme = cx.theme().clone();
4789 let colors = theme.colors();
4790
4791 client_side_decorations(
4792 self.actions(div(), cx)
4793 .key_context(context)
4794 .relative()
4795 .size_full()
4796 .flex()
4797 .flex_col()
4798 .font(ui_font)
4799 .gap_0()
4800 .justify_start()
4801 .items_start()
4802 .text_color(colors.text)
4803 .overflow_hidden()
4804 .children(self.titlebar_item.clone())
4805 .child(
4806 div()
4807 .size_full()
4808 .relative()
4809 .flex_1()
4810 .flex()
4811 .flex_col()
4812 .child(
4813 div()
4814 .id("workspace")
4815 .bg(colors.background)
4816 .relative()
4817 .flex_1()
4818 .w_full()
4819 .flex()
4820 .flex_col()
4821 .overflow_hidden()
4822 .border_t_1()
4823 .border_b_1()
4824 .border_color(colors.border)
4825 .child({
4826 let this = cx.view().clone();
4827 canvas(
4828 move |bounds, cx| {
4829 this.update(cx, |this, cx| {
4830 let bounds_changed = this.bounds != bounds;
4831 this.bounds = bounds;
4832
4833 if bounds_changed {
4834 this.left_dock.update(cx, |dock, cx| {
4835 dock.clamp_panel_size(bounds.size.width, cx)
4836 });
4837
4838 this.right_dock.update(cx, |dock, cx| {
4839 dock.clamp_panel_size(bounds.size.width, cx)
4840 });
4841
4842 this.bottom_dock.update(cx, |dock, cx| {
4843 dock.clamp_panel_size(
4844 bounds.size.height,
4845 cx,
4846 )
4847 });
4848 }
4849 })
4850 },
4851 |_, _, _| {},
4852 )
4853 .absolute()
4854 .size_full()
4855 })
4856 .when(self.zoomed.is_none(), |this| {
4857 this.on_drag_move(cx.listener(
4858 |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4859 match e.drag(cx).0 {
4860 DockPosition::Left => {
4861 resize_left_dock(
4862 e.event.position.x
4863 - workspace.bounds.left(),
4864 workspace,
4865 cx,
4866 );
4867 }
4868 DockPosition::Right => {
4869 resize_right_dock(
4870 workspace.bounds.right()
4871 - e.event.position.x,
4872 workspace,
4873 cx,
4874 );
4875 }
4876 DockPosition::Bottom => {
4877 resize_bottom_dock(
4878 workspace.bounds.bottom()
4879 - e.event.position.y,
4880 workspace,
4881 cx,
4882 );
4883 }
4884 }
4885 },
4886 ))
4887 })
4888 .child(
4889 div()
4890 .flex()
4891 .flex_row()
4892 .h_full()
4893 // Left Dock
4894 .children(self.render_dock(
4895 DockPosition::Left,
4896 &self.left_dock,
4897 cx,
4898 ))
4899 // Panes
4900 .child(
4901 div()
4902 .flex()
4903 .flex_col()
4904 .flex_1()
4905 .overflow_hidden()
4906 .child(
4907 h_flex()
4908 .flex_1()
4909 .when_some(paddings.0, |this, p| {
4910 this.child(p.border_r_1())
4911 })
4912 .child(self.center.render(
4913 &self.project,
4914 &self.follower_states,
4915 self.active_call(),
4916 &self.active_pane,
4917 self.zoomed.as_ref(),
4918 &self.app_state,
4919 cx,
4920 ))
4921 .when_some(paddings.1, |this, p| {
4922 this.child(p.border_l_1())
4923 }),
4924 )
4925 .children(self.render_dock(
4926 DockPosition::Bottom,
4927 &self.bottom_dock,
4928 cx,
4929 )),
4930 )
4931 // Right Dock
4932 .children(self.render_dock(
4933 DockPosition::Right,
4934 &self.right_dock,
4935 cx,
4936 )),
4937 )
4938 .children(self.zoomed.as_ref().and_then(|view| {
4939 let zoomed_view = view.upgrade()?;
4940 let div = div()
4941 .occlude()
4942 .absolute()
4943 .overflow_hidden()
4944 .border_color(colors.border)
4945 .bg(colors.background)
4946 .child(zoomed_view)
4947 .inset_0()
4948 .shadow_lg();
4949
4950 Some(match self.zoomed_position {
4951 Some(DockPosition::Left) => div.right_2().border_r_1(),
4952 Some(DockPosition::Right) => div.left_2().border_l_1(),
4953 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4954 None => {
4955 div.top_2().bottom_2().left_2().right_2().border_1()
4956 }
4957 })
4958 }))
4959 .children(self.render_notifications(cx)),
4960 )
4961 .child(self.status_bar.clone())
4962 .child(self.modal_layer.clone()),
4963 ),
4964 cx,
4965 )
4966 }
4967}
4968
4969fn resize_bottom_dock(
4970 new_size: Pixels,
4971 workspace: &mut Workspace,
4972 cx: &mut ViewContext<'_, Workspace>,
4973) {
4974 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
4975 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
4976 bottom_dock.resize_active_panel(Some(size), cx);
4977 });
4978}
4979
4980fn resize_right_dock(
4981 new_size: Pixels,
4982 workspace: &mut Workspace,
4983 cx: &mut ViewContext<'_, Workspace>,
4984) {
4985 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
4986 workspace.right_dock.update(cx, |right_dock, cx| {
4987 right_dock.resize_active_panel(Some(size), cx);
4988 });
4989}
4990
4991fn resize_left_dock(
4992 new_size: Pixels,
4993 workspace: &mut Workspace,
4994 cx: &mut ViewContext<'_, Workspace>,
4995) {
4996 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
4997
4998 workspace.left_dock.update(cx, |left_dock, cx| {
4999 left_dock.resize_active_panel(Some(size), cx);
5000 });
5001}
5002
5003impl WorkspaceStore {
5004 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
5005 Self {
5006 workspaces: Default::default(),
5007 _subscriptions: vec![
5008 client.add_request_handler(cx.weak_model(), Self::handle_follow),
5009 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
5010 ],
5011 client,
5012 }
5013 }
5014
5015 pub fn update_followers(
5016 &self,
5017 project_id: Option<u64>,
5018 update: proto::update_followers::Variant,
5019 cx: &AppContext,
5020 ) -> Option<()> {
5021 let active_call = ActiveCall::try_global(cx)?;
5022 let room_id = active_call.read(cx).room()?.read(cx).id();
5023 self.client
5024 .send(proto::UpdateFollowers {
5025 room_id,
5026 project_id,
5027 variant: Some(update),
5028 })
5029 .log_err()
5030 }
5031
5032 pub async fn handle_follow(
5033 this: Model<Self>,
5034 envelope: TypedEnvelope<proto::Follow>,
5035 mut cx: AsyncAppContext,
5036 ) -> Result<proto::FollowResponse> {
5037 this.update(&mut cx, |this, cx| {
5038 let follower = Follower {
5039 project_id: envelope.payload.project_id,
5040 peer_id: envelope.original_sender_id()?,
5041 };
5042
5043 let mut response = proto::FollowResponse::default();
5044 this.workspaces.retain(|workspace| {
5045 workspace
5046 .update(cx, |workspace, cx| {
5047 let handler_response = workspace.handle_follow(follower.project_id, cx);
5048 if let Some(active_view) = handler_response.active_view.clone() {
5049 if workspace.project.read(cx).remote_id() == follower.project_id {
5050 response.active_view = Some(active_view)
5051 }
5052 }
5053 })
5054 .is_ok()
5055 });
5056
5057 Ok(response)
5058 })?
5059 }
5060
5061 async fn handle_update_followers(
5062 this: Model<Self>,
5063 envelope: TypedEnvelope<proto::UpdateFollowers>,
5064 mut cx: AsyncAppContext,
5065 ) -> Result<()> {
5066 let leader_id = envelope.original_sender_id()?;
5067 let update = envelope.payload;
5068
5069 this.update(&mut cx, |this, cx| {
5070 this.workspaces.retain(|workspace| {
5071 workspace
5072 .update(cx, |workspace, cx| {
5073 let project_id = workspace.project.read(cx).remote_id();
5074 if update.project_id != project_id && update.project_id.is_some() {
5075 return;
5076 }
5077 workspace.handle_update_followers(leader_id, update.clone(), cx);
5078 })
5079 .is_ok()
5080 });
5081 Ok(())
5082 })?
5083 }
5084}
5085
5086impl ViewId {
5087 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5088 Ok(Self {
5089 creator: message
5090 .creator
5091 .ok_or_else(|| anyhow!("creator is missing"))?,
5092 id: message.id,
5093 })
5094 }
5095
5096 pub(crate) fn to_proto(self) -> proto::ViewId {
5097 proto::ViewId {
5098 creator: Some(self.creator),
5099 id: self.id,
5100 }
5101 }
5102}
5103
5104impl FollowerState {
5105 fn pane(&self) -> &View<Pane> {
5106 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5107 }
5108}
5109
5110pub trait WorkspaceHandle {
5111 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5112}
5113
5114impl WorkspaceHandle for View<Workspace> {
5115 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5116 self.read(cx)
5117 .worktrees(cx)
5118 .flat_map(|worktree| {
5119 let worktree_id = worktree.read(cx).id();
5120 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5121 worktree_id,
5122 path: f.path.clone(),
5123 })
5124 })
5125 .collect::<Vec<_>>()
5126 }
5127}
5128
5129impl std::fmt::Debug for OpenPaths {
5130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5131 f.debug_struct("OpenPaths")
5132 .field("paths", &self.paths)
5133 .finish()
5134 }
5135}
5136
5137pub fn activate_workspace_for_project(
5138 cx: &mut AppContext,
5139 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5140) -> Option<WindowHandle<Workspace>> {
5141 for window in cx.windows() {
5142 let Some(workspace) = window.downcast::<Workspace>() else {
5143 continue;
5144 };
5145
5146 let predicate = workspace
5147 .update(cx, |workspace, cx| {
5148 let project = workspace.project.read(cx);
5149 if predicate(project, cx) {
5150 cx.activate_window();
5151 true
5152 } else {
5153 false
5154 }
5155 })
5156 .log_err()
5157 .unwrap_or(false);
5158
5159 if predicate {
5160 return Some(workspace);
5161 }
5162 }
5163
5164 None
5165}
5166
5167pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5168 DB.last_workspace().await.log_err().flatten()
5169}
5170
5171pub fn last_session_workspace_locations(
5172 last_session_id: &str,
5173 last_session_window_stack: Option<Vec<WindowId>>,
5174) -> Option<Vec<SerializedWorkspaceLocation>> {
5175 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5176 .log_err()
5177}
5178
5179actions!(collab, [OpenChannelNotes]);
5180actions!(zed, [OpenLog]);
5181
5182async fn join_channel_internal(
5183 channel_id: ChannelId,
5184 app_state: &Arc<AppState>,
5185 requesting_window: Option<WindowHandle<Workspace>>,
5186 active_call: &Model<ActiveCall>,
5187 cx: &mut AsyncAppContext,
5188) -> Result<bool> {
5189 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5190 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5191 return (false, None);
5192 };
5193
5194 let already_in_channel = room.channel_id() == Some(channel_id);
5195 let should_prompt = room.is_sharing_project()
5196 && !room.remote_participants().is_empty()
5197 && !already_in_channel;
5198 let open_room = if already_in_channel {
5199 active_call.room().cloned()
5200 } else {
5201 None
5202 };
5203 (should_prompt, open_room)
5204 })?;
5205
5206 if let Some(room) = open_room {
5207 let task = room.update(cx, |room, cx| {
5208 if let Some((project, host)) = room.most_active_project(cx) {
5209 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5210 }
5211
5212 None
5213 })?;
5214 if let Some(task) = task {
5215 task.await?;
5216 }
5217 return anyhow::Ok(true);
5218 }
5219
5220 if should_prompt {
5221 if let Some(workspace) = requesting_window {
5222 let answer = workspace
5223 .update(cx, |_, cx| {
5224 cx.prompt(
5225 PromptLevel::Warning,
5226 "Do you want to switch channels?",
5227 Some("Leaving this call will unshare your current project."),
5228 &["Yes, Join Channel", "Cancel"],
5229 )
5230 })?
5231 .await;
5232
5233 if answer == Ok(1) {
5234 return Ok(false);
5235 }
5236 } else {
5237 return Ok(false); // unreachable!() hopefully
5238 }
5239 }
5240
5241 let client = cx.update(|cx| active_call.read(cx).client())?;
5242
5243 let mut client_status = client.status();
5244
5245 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5246 'outer: loop {
5247 let Some(status) = client_status.recv().await else {
5248 return Err(anyhow!("error connecting"));
5249 };
5250
5251 match status {
5252 Status::Connecting
5253 | Status::Authenticating
5254 | Status::Reconnecting
5255 | Status::Reauthenticating => continue,
5256 Status::Connected { .. } => break 'outer,
5257 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5258 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5259 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5260 return Err(ErrorCode::Disconnected.into());
5261 }
5262 }
5263 }
5264
5265 let room = active_call
5266 .update(cx, |active_call, cx| {
5267 active_call.join_channel(channel_id, cx)
5268 })?
5269 .await?;
5270
5271 let Some(room) = room else {
5272 return anyhow::Ok(true);
5273 };
5274
5275 room.update(cx, |room, _| room.room_update_completed())?
5276 .await;
5277
5278 let task = room.update(cx, |room, cx| {
5279 if let Some((project, host)) = room.most_active_project(cx) {
5280 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5281 }
5282
5283 // If you are the first to join a channel, see if you should share your project.
5284 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5285 if let Some(workspace) = requesting_window {
5286 let project = workspace.update(cx, |workspace, cx| {
5287 let project = workspace.project.read(cx);
5288
5289 if !CallSettings::get_global(cx).share_on_join {
5290 return None;
5291 }
5292
5293 if (project.is_local() || project.is_via_ssh())
5294 && project.visible_worktrees(cx).any(|tree| {
5295 tree.read(cx)
5296 .root_entry()
5297 .map_or(false, |entry| entry.is_dir())
5298 })
5299 {
5300 Some(workspace.project.clone())
5301 } else {
5302 None
5303 }
5304 });
5305 if let Ok(Some(project)) = project {
5306 return Some(cx.spawn(|room, mut cx| async move {
5307 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5308 .await?;
5309 Ok(())
5310 }));
5311 }
5312 }
5313 }
5314
5315 None
5316 })?;
5317 if let Some(task) = task {
5318 task.await?;
5319 return anyhow::Ok(true);
5320 }
5321 anyhow::Ok(false)
5322}
5323
5324pub fn join_channel(
5325 channel_id: ChannelId,
5326 app_state: Arc<AppState>,
5327 requesting_window: Option<WindowHandle<Workspace>>,
5328 cx: &mut AppContext,
5329) -> Task<Result<()>> {
5330 let active_call = ActiveCall::global(cx);
5331 cx.spawn(|mut cx| async move {
5332 let result = join_channel_internal(
5333 channel_id,
5334 &app_state,
5335 requesting_window,
5336 &active_call,
5337 &mut cx,
5338 )
5339 .await;
5340
5341 // join channel succeeded, and opened a window
5342 if matches!(result, Ok(true)) {
5343 return anyhow::Ok(());
5344 }
5345
5346 // find an existing workspace to focus and show call controls
5347 let mut active_window =
5348 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5349 if active_window.is_none() {
5350 // no open workspaces, make one to show the error in (blergh)
5351 let (window_handle, _) = cx
5352 .update(|cx| {
5353 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5354 })?
5355 .await?;
5356
5357 if result.is_ok() {
5358 cx.update(|cx| {
5359 cx.dispatch_action(&OpenChannelNotes);
5360 }).log_err();
5361 }
5362
5363 active_window = Some(window_handle);
5364 }
5365
5366 if let Err(err) = result {
5367 log::error!("failed to join channel: {}", err);
5368 if let Some(active_window) = active_window {
5369 active_window
5370 .update(&mut cx, |_, cx| {
5371 let detail: SharedString = match err.error_code() {
5372 ErrorCode::SignedOut => {
5373 "Please sign in to continue.".into()
5374 }
5375 ErrorCode::UpgradeRequired => {
5376 "Your are running an unsupported version of Zed. Please update to continue.".into()
5377 }
5378 ErrorCode::NoSuchChannel => {
5379 "No matching channel was found. Please check the link and try again.".into()
5380 }
5381 ErrorCode::Forbidden => {
5382 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5383 }
5384 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5385 _ => format!("{}\n\nPlease try again.", err).into(),
5386 };
5387 cx.prompt(
5388 PromptLevel::Critical,
5389 "Failed to join channel",
5390 Some(&detail),
5391 &["Ok"],
5392 )
5393 })?
5394 .await
5395 .ok();
5396 }
5397 }
5398
5399 // return ok, we showed the error to the user.
5400 anyhow::Ok(())
5401 })
5402}
5403
5404pub async fn get_any_active_workspace(
5405 app_state: Arc<AppState>,
5406 mut cx: AsyncAppContext,
5407) -> anyhow::Result<WindowHandle<Workspace>> {
5408 // find an existing workspace to focus and show call controls
5409 let active_window = activate_any_workspace_window(&mut cx);
5410 if active_window.is_none() {
5411 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5412 .await?;
5413 }
5414 activate_any_workspace_window(&mut cx).context("could not open zed")
5415}
5416
5417fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5418 cx.update(|cx| {
5419 if let Some(workspace_window) = cx
5420 .active_window()
5421 .and_then(|window| window.downcast::<Workspace>())
5422 {
5423 return Some(workspace_window);
5424 }
5425
5426 for window in cx.windows() {
5427 if let Some(workspace_window) = window.downcast::<Workspace>() {
5428 workspace_window
5429 .update(cx, |_, cx| cx.activate_window())
5430 .ok();
5431 return Some(workspace_window);
5432 }
5433 }
5434 None
5435 })
5436 .ok()
5437 .flatten()
5438}
5439
5440pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5441 cx.windows()
5442 .into_iter()
5443 .filter_map(|window| window.downcast::<Workspace>())
5444 .filter(|workspace| {
5445 workspace
5446 .read(cx)
5447 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5448 })
5449 .collect()
5450}
5451
5452#[derive(Default)]
5453pub struct OpenOptions {
5454 pub open_new_workspace: Option<bool>,
5455 pub replace_window: Option<WindowHandle<Workspace>>,
5456 pub env: Option<HashMap<String, String>>,
5457}
5458
5459#[allow(clippy::type_complexity)]
5460pub fn open_paths(
5461 abs_paths: &[PathBuf],
5462 app_state: Arc<AppState>,
5463 open_options: OpenOptions,
5464 cx: &mut AppContext,
5465) -> Task<
5466 anyhow::Result<(
5467 WindowHandle<Workspace>,
5468 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5469 )>,
5470> {
5471 let abs_paths = abs_paths.to_vec();
5472 let mut existing = None;
5473 let mut best_match = None;
5474 let mut open_visible = OpenVisible::All;
5475
5476 if open_options.open_new_workspace != Some(true) {
5477 for window in local_workspace_windows(cx) {
5478 if let Ok(workspace) = window.read(cx) {
5479 let m = workspace
5480 .project
5481 .read(cx)
5482 .visibility_for_paths(&abs_paths, cx);
5483 if m > best_match {
5484 existing = Some(window);
5485 best_match = m;
5486 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5487 existing = Some(window)
5488 }
5489 }
5490 }
5491 }
5492
5493 cx.spawn(move |mut cx| async move {
5494 if open_options.open_new_workspace.is_none() && existing.is_none() {
5495 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5496 if futures::future::join_all(all_files)
5497 .await
5498 .into_iter()
5499 .filter_map(|result| result.ok().flatten())
5500 .all(|file| !file.is_dir)
5501 {
5502 cx.update(|cx| {
5503 for window in local_workspace_windows(cx) {
5504 if let Ok(workspace) = window.read(cx) {
5505 let project = workspace.project().read(cx);
5506 if project.is_via_collab() {
5507 continue;
5508 }
5509 existing = Some(window);
5510 open_visible = OpenVisible::None;
5511 break;
5512 }
5513 }
5514 })?;
5515 }
5516 }
5517
5518 if let Some(existing) = existing {
5519 Ok((
5520 existing,
5521 existing
5522 .update(&mut cx, |workspace, cx| {
5523 cx.activate_window();
5524 workspace.open_paths(abs_paths, open_visible, None, cx)
5525 })?
5526 .await,
5527 ))
5528 } else {
5529 cx.update(move |cx| {
5530 Workspace::new_local(
5531 abs_paths,
5532 app_state.clone(),
5533 open_options.replace_window,
5534 open_options.env,
5535 cx,
5536 )
5537 })?
5538 .await
5539 }
5540 })
5541}
5542
5543pub fn open_new(
5544 open_options: OpenOptions,
5545 app_state: Arc<AppState>,
5546 cx: &mut AppContext,
5547 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5548) -> Task<anyhow::Result<()>> {
5549 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5550 cx.spawn(|mut cx| async move {
5551 let (workspace, opened_paths) = task.await?;
5552 workspace.update(&mut cx, |workspace, cx| {
5553 if opened_paths.is_empty() {
5554 init(workspace, cx)
5555 }
5556 })?;
5557 Ok(())
5558 })
5559}
5560
5561pub fn create_and_open_local_file(
5562 path: &'static Path,
5563 cx: &mut ViewContext<Workspace>,
5564 default_content: impl 'static + Send + FnOnce() -> Rope,
5565) -> Task<Result<Box<dyn ItemHandle>>> {
5566 cx.spawn(|workspace, mut cx| async move {
5567 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5568 if !fs.is_file(path).await {
5569 fs.create_file(path, Default::default()).await?;
5570 fs.save(path, &default_content(), Default::default())
5571 .await?;
5572 }
5573
5574 let mut items = workspace
5575 .update(&mut cx, |workspace, cx| {
5576 workspace.with_local_workspace(cx, |workspace, cx| {
5577 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5578 })
5579 })?
5580 .await?
5581 .await;
5582
5583 let item = items.pop().flatten();
5584 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5585 })
5586}
5587
5588pub fn open_ssh_project(
5589 window: WindowHandle<Workspace>,
5590 connection_options: SshConnectionOptions,
5591 cancel_rx: oneshot::Receiver<()>,
5592 delegate: Arc<dyn SshClientDelegate>,
5593 app_state: Arc<AppState>,
5594 paths: Vec<PathBuf>,
5595 cx: &mut AppContext,
5596) -> Task<Result<()>> {
5597 cx.spawn(|mut cx| async move {
5598 let (serialized_ssh_project, workspace_id, serialized_workspace) =
5599 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5600
5601 let session = match cx
5602 .update(|cx| {
5603 remote::SshRemoteClient::new(
5604 ConnectionIdentifier::Workspace(workspace_id.0),
5605 connection_options,
5606 cancel_rx,
5607 delegate,
5608 cx,
5609 )
5610 })?
5611 .await?
5612 {
5613 Some(result) => result,
5614 None => return Ok(()),
5615 };
5616
5617 let project = cx.update(|cx| {
5618 project::Project::ssh(
5619 session,
5620 app_state.client.clone(),
5621 app_state.node_runtime.clone(),
5622 app_state.user_store.clone(),
5623 app_state.languages.clone(),
5624 app_state.fs.clone(),
5625 cx,
5626 )
5627 })?;
5628
5629 let toolchains = DB.toolchains(workspace_id).await?;
5630 for (toolchain, worktree_id) in toolchains {
5631 project
5632 .update(&mut cx, |this, cx| {
5633 this.activate_toolchain(worktree_id, toolchain, cx)
5634 })?
5635 .await;
5636 }
5637 let mut project_paths_to_open = vec![];
5638 let mut project_path_errors = vec![];
5639
5640 for path in paths {
5641 let result = cx
5642 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5643 .await;
5644 match result {
5645 Ok((_, project_path)) => {
5646 project_paths_to_open.push((path.clone(), Some(project_path)));
5647 }
5648 Err(error) => {
5649 project_path_errors.push(error);
5650 }
5651 };
5652 }
5653
5654 if project_paths_to_open.is_empty() {
5655 return Err(project_path_errors
5656 .pop()
5657 .unwrap_or_else(|| anyhow!("no paths given")));
5658 }
5659
5660 cx.update_window(window.into(), |_, cx| {
5661 cx.replace_root_view(|cx| {
5662 let mut workspace =
5663 Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5664
5665 workspace
5666 .client()
5667 .telemetry()
5668 .report_app_event("open ssh project".to_string());
5669
5670 workspace.set_serialized_ssh_project(serialized_ssh_project);
5671 workspace
5672 });
5673 })?;
5674
5675 window
5676 .update(&mut cx, |_, cx| {
5677 cx.activate_window();
5678
5679 open_items(serialized_workspace, project_paths_to_open, cx)
5680 })?
5681 .await?;
5682
5683 window.update(&mut cx, |workspace, cx| {
5684 for error in project_path_errors {
5685 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5686 if let Some(path) = error.error_tag("path") {
5687 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5688 }
5689 } else {
5690 workspace.show_error(&error, cx)
5691 }
5692 }
5693 })
5694 })
5695}
5696
5697fn serialize_ssh_project(
5698 connection_options: SshConnectionOptions,
5699 paths: Vec<PathBuf>,
5700 cx: &AsyncAppContext,
5701) -> Task<
5702 Result<(
5703 SerializedSshProject,
5704 WorkspaceId,
5705 Option<SerializedWorkspace>,
5706 )>,
5707> {
5708 cx.background_executor().spawn(async move {
5709 let serialized_ssh_project = persistence::DB
5710 .get_or_create_ssh_project(
5711 connection_options.host.clone(),
5712 connection_options.port,
5713 paths
5714 .iter()
5715 .map(|path| path.to_string_lossy().to_string())
5716 .collect::<Vec<_>>(),
5717 connection_options.username.clone(),
5718 )
5719 .await?;
5720
5721 let serialized_workspace =
5722 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5723
5724 let workspace_id = if let Some(workspace_id) =
5725 serialized_workspace.as_ref().map(|workspace| workspace.id)
5726 {
5727 workspace_id
5728 } else {
5729 persistence::DB.next_id().await?
5730 };
5731
5732 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5733 })
5734}
5735
5736pub fn join_in_room_project(
5737 project_id: u64,
5738 follow_user_id: u64,
5739 app_state: Arc<AppState>,
5740 cx: &mut AppContext,
5741) -> Task<Result<()>> {
5742 let windows = cx.windows();
5743 cx.spawn(|mut cx| async move {
5744 let existing_workspace = windows.into_iter().find_map(|window| {
5745 window.downcast::<Workspace>().and_then(|window| {
5746 window
5747 .update(&mut cx, |workspace, cx| {
5748 if workspace.project().read(cx).remote_id() == Some(project_id) {
5749 Some(window)
5750 } else {
5751 None
5752 }
5753 })
5754 .unwrap_or(None)
5755 })
5756 });
5757
5758 let workspace = if let Some(existing_workspace) = existing_workspace {
5759 existing_workspace
5760 } else {
5761 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5762 let room = active_call
5763 .read_with(&cx, |call, _| call.room().cloned())?
5764 .ok_or_else(|| anyhow!("not in a call"))?;
5765 let project = room
5766 .update(&mut cx, |room, cx| {
5767 room.join_project(
5768 project_id,
5769 app_state.languages.clone(),
5770 app_state.fs.clone(),
5771 cx,
5772 )
5773 })?
5774 .await?;
5775
5776 let window_bounds_override = window_bounds_env_override();
5777 cx.update(|cx| {
5778 let mut options = (app_state.build_window_options)(None, cx);
5779 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5780 cx.open_window(options, |cx| {
5781 cx.new_view(|cx| {
5782 Workspace::new(Default::default(), project, app_state.clone(), cx)
5783 })
5784 })
5785 })??
5786 };
5787
5788 workspace.update(&mut cx, |workspace, cx| {
5789 cx.activate(true);
5790 cx.activate_window();
5791
5792 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5793 let follow_peer_id = room
5794 .read(cx)
5795 .remote_participants()
5796 .iter()
5797 .find(|(_, participant)| participant.user.id == follow_user_id)
5798 .map(|(_, p)| p.peer_id)
5799 .or_else(|| {
5800 // If we couldn't follow the given user, follow the host instead.
5801 let collaborator = workspace
5802 .project()
5803 .read(cx)
5804 .collaborators()
5805 .values()
5806 .find(|collaborator| collaborator.is_host)?;
5807 Some(collaborator.peer_id)
5808 });
5809
5810 if let Some(follow_peer_id) = follow_peer_id {
5811 workspace.follow(follow_peer_id, cx);
5812 }
5813 }
5814 })?;
5815
5816 anyhow::Ok(())
5817 })
5818}
5819
5820pub fn reload(reload: &Reload, cx: &mut AppContext) {
5821 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5822 let mut workspace_windows = cx
5823 .windows()
5824 .into_iter()
5825 .filter_map(|window| window.downcast::<Workspace>())
5826 .collect::<Vec<_>>();
5827
5828 // If multiple windows have unsaved changes, and need a save prompt,
5829 // prompt in the active window before switching to a different window.
5830 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5831
5832 let mut prompt = None;
5833 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5834 prompt = window
5835 .update(cx, |_, cx| {
5836 cx.prompt(
5837 PromptLevel::Info,
5838 "Are you sure you want to restart?",
5839 None,
5840 &["Restart", "Cancel"],
5841 )
5842 })
5843 .ok();
5844 }
5845
5846 let binary_path = reload.binary_path.clone();
5847 cx.spawn(|mut cx| async move {
5848 if let Some(prompt) = prompt {
5849 let answer = prompt.await?;
5850 if answer != 0 {
5851 return Ok(());
5852 }
5853 }
5854
5855 // If the user cancels any save prompt, then keep the app open.
5856 for window in workspace_windows {
5857 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5858 workspace.prepare_to_close(CloseIntent::Quit, cx)
5859 }) {
5860 if !should_close.await? {
5861 return Ok(());
5862 }
5863 }
5864 }
5865
5866 cx.update(|cx| cx.restart(binary_path))
5867 })
5868 .detach_and_log_err(cx);
5869}
5870
5871fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5872 let mut parts = value.split(',');
5873 let x: usize = parts.next()?.parse().ok()?;
5874 let y: usize = parts.next()?.parse().ok()?;
5875 Some(point(px(x as f32), px(y as f32)))
5876}
5877
5878fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5879 let mut parts = value.split(',');
5880 let width: usize = parts.next()?.parse().ok()?;
5881 let height: usize = parts.next()?.parse().ok()?;
5882 Some(size(px(width as f32), px(height as f32)))
5883}
5884
5885pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5886 const BORDER_SIZE: Pixels = px(1.0);
5887 let decorations = cx.window_decorations();
5888
5889 if matches!(decorations, Decorations::Client { .. }) {
5890 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5891 }
5892
5893 struct GlobalResizeEdge(ResizeEdge);
5894 impl Global for GlobalResizeEdge {}
5895
5896 div()
5897 .id("window-backdrop")
5898 .bg(transparent_black())
5899 .map(|div| match decorations {
5900 Decorations::Server => div,
5901 Decorations::Client { tiling, .. } => div
5902 .when(!(tiling.top || tiling.right), |div| {
5903 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5904 })
5905 .when(!(tiling.top || tiling.left), |div| {
5906 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5907 })
5908 .when(!(tiling.bottom || tiling.right), |div| {
5909 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5910 })
5911 .when(!(tiling.bottom || tiling.left), |div| {
5912 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5913 })
5914 .when(!tiling.top, |div| {
5915 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5916 })
5917 .when(!tiling.bottom, |div| {
5918 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5919 })
5920 .when(!tiling.left, |div| {
5921 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5922 })
5923 .when(!tiling.right, |div| {
5924 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5925 })
5926 .on_mouse_move(move |e, cx| {
5927 let size = cx.window_bounds().get_bounds().size;
5928 let pos = e.position;
5929
5930 let new_edge =
5931 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5932
5933 let edge = cx.try_global::<GlobalResizeEdge>();
5934 if new_edge != edge.map(|edge| edge.0) {
5935 cx.window_handle()
5936 .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
5937 .ok();
5938 }
5939 })
5940 .on_mouse_down(MouseButton::Left, move |e, cx| {
5941 let size = cx.window_bounds().get_bounds().size;
5942 let pos = e.position;
5943
5944 let edge = match resize_edge(
5945 pos,
5946 theme::CLIENT_SIDE_DECORATION_SHADOW,
5947 size,
5948 tiling,
5949 ) {
5950 Some(value) => value,
5951 None => return,
5952 };
5953
5954 cx.start_window_resize(edge);
5955 }),
5956 })
5957 .size_full()
5958 .child(
5959 div()
5960 .cursor(CursorStyle::Arrow)
5961 .map(|div| match decorations {
5962 Decorations::Server => div,
5963 Decorations::Client { tiling } => div
5964 .border_color(cx.theme().colors().border)
5965 .when(!(tiling.top || tiling.right), |div| {
5966 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5967 })
5968 .when(!(tiling.top || tiling.left), |div| {
5969 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5970 })
5971 .when(!(tiling.bottom || tiling.right), |div| {
5972 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5973 })
5974 .when(!(tiling.bottom || tiling.left), |div| {
5975 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5976 })
5977 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
5978 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
5979 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
5980 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
5981 .when(!tiling.is_tiled(), |div| {
5982 div.shadow(smallvec::smallvec![gpui::BoxShadow {
5983 color: Hsla {
5984 h: 0.,
5985 s: 0.,
5986 l: 0.,
5987 a: 0.4,
5988 },
5989 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
5990 spread_radius: px(0.),
5991 offset: point(px(0.0), px(0.0)),
5992 }])
5993 }),
5994 })
5995 .on_mouse_move(|_e, cx| {
5996 cx.stop_propagation();
5997 })
5998 .size_full()
5999 .child(element),
6000 )
6001 .map(|div| match decorations {
6002 Decorations::Server => div,
6003 Decorations::Client { tiling, .. } => div.child(
6004 canvas(
6005 |_bounds, cx| {
6006 cx.insert_hitbox(
6007 Bounds::new(
6008 point(px(0.0), px(0.0)),
6009 cx.window_bounds().get_bounds().size,
6010 ),
6011 false,
6012 )
6013 },
6014 move |_bounds, hitbox, cx| {
6015 let mouse = cx.mouse_position();
6016 let size = cx.window_bounds().get_bounds().size;
6017 let Some(edge) =
6018 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6019 else {
6020 return;
6021 };
6022 cx.set_global(GlobalResizeEdge(edge));
6023 cx.set_cursor_style(
6024 match edge {
6025 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6026 ResizeEdge::Left | ResizeEdge::Right => {
6027 CursorStyle::ResizeLeftRight
6028 }
6029 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6030 CursorStyle::ResizeUpLeftDownRight
6031 }
6032 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6033 CursorStyle::ResizeUpRightDownLeft
6034 }
6035 },
6036 &hitbox,
6037 );
6038 },
6039 )
6040 .size_full()
6041 .absolute(),
6042 ),
6043 })
6044}
6045
6046fn resize_edge(
6047 pos: Point<Pixels>,
6048 shadow_size: Pixels,
6049 window_size: Size<Pixels>,
6050 tiling: Tiling,
6051) -> Option<ResizeEdge> {
6052 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6053 if bounds.contains(&pos) {
6054 return None;
6055 }
6056
6057 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6058 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6059 if !tiling.top && top_left_bounds.contains(&pos) {
6060 return Some(ResizeEdge::TopLeft);
6061 }
6062
6063 let top_right_bounds = Bounds::new(
6064 Point::new(window_size.width - corner_size.width, px(0.)),
6065 corner_size,
6066 );
6067 if !tiling.top && top_right_bounds.contains(&pos) {
6068 return Some(ResizeEdge::TopRight);
6069 }
6070
6071 let bottom_left_bounds = Bounds::new(
6072 Point::new(px(0.), window_size.height - corner_size.height),
6073 corner_size,
6074 );
6075 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6076 return Some(ResizeEdge::BottomLeft);
6077 }
6078
6079 let bottom_right_bounds = Bounds::new(
6080 Point::new(
6081 window_size.width - corner_size.width,
6082 window_size.height - corner_size.height,
6083 ),
6084 corner_size,
6085 );
6086 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6087 return Some(ResizeEdge::BottomRight);
6088 }
6089
6090 if !tiling.top && pos.y < shadow_size {
6091 Some(ResizeEdge::Top)
6092 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6093 Some(ResizeEdge::Bottom)
6094 } else if !tiling.left && pos.x < shadow_size {
6095 Some(ResizeEdge::Left)
6096 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6097 Some(ResizeEdge::Right)
6098 } else {
6099 None
6100 }
6101}
6102
6103fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6104 if pane == active_pane {
6105 return;
6106 } else if pane.read(cx).items_len() == 0 {
6107 pane.update(cx, |_, cx| {
6108 cx.emit(pane::Event::Remove {
6109 focus_on_pane: None,
6110 });
6111 })
6112 } else {
6113 move_all_items(pane, active_pane, cx);
6114 }
6115}
6116
6117fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6118 let destination_is_different = from_pane != to_pane;
6119 let mut moved_items = 0;
6120 for (item_ix, item_handle) in from_pane
6121 .read(cx)
6122 .items()
6123 .enumerate()
6124 .map(|(ix, item)| (ix, item.clone()))
6125 .collect::<Vec<_>>()
6126 {
6127 let ix = item_ix - moved_items;
6128 if destination_is_different {
6129 // Close item from previous pane
6130 from_pane.update(cx, |source, cx| {
6131 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6132 });
6133 moved_items += 1;
6134 }
6135
6136 // This automatically removes duplicate items in the pane
6137 to_pane.update(cx, |destination, cx| {
6138 destination.add_item(item_handle, true, true, None, cx);
6139 destination.focus(cx)
6140 });
6141 }
6142}
6143
6144pub fn move_item(
6145 source: &View<Pane>,
6146 destination: &View<Pane>,
6147 item_id_to_move: EntityId,
6148 destination_index: usize,
6149 cx: &mut WindowContext<'_>,
6150) {
6151 let Some((item_ix, item_handle)) = source
6152 .read(cx)
6153 .items()
6154 .enumerate()
6155 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6156 .map(|(ix, item)| (ix, item.clone()))
6157 else {
6158 // Tab was closed during drag
6159 return;
6160 };
6161
6162 if source != destination {
6163 // Close item from previous pane
6164 source.update(cx, |source, cx| {
6165 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6166 });
6167 }
6168
6169 // This automatically removes duplicate items in the pane
6170 destination.update(cx, |destination, cx| {
6171 destination.add_item(item_handle, true, true, Some(destination_index), cx);
6172 destination.focus(cx)
6173 });
6174}
6175
6176#[cfg(test)]
6177mod tests {
6178 use std::{cell::RefCell, rc::Rc};
6179
6180 use super::*;
6181 use crate::{
6182 dock::{test::TestPanel, PanelEvent},
6183 item::{
6184 test::{TestItem, TestProjectItem},
6185 ItemEvent,
6186 },
6187 };
6188 use fs::FakeFs;
6189 use gpui::{
6190 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6191 UpdateGlobal, VisualTestContext,
6192 };
6193 use project::{Project, ProjectEntryId};
6194 use serde_json::json;
6195 use settings::SettingsStore;
6196
6197 #[gpui::test]
6198 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6199 init_test(cx);
6200
6201 let fs = FakeFs::new(cx.executor());
6202 let project = Project::test(fs, [], cx).await;
6203 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6204
6205 // Adding an item with no ambiguity renders the tab without detail.
6206 let item1 = cx.new_view(|cx| {
6207 let mut item = TestItem::new(cx);
6208 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6209 item
6210 });
6211 workspace.update(cx, |workspace, cx| {
6212 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6213 });
6214 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6215
6216 // Adding an item that creates ambiguity increases the level of detail on
6217 // both tabs.
6218 let item2 = cx.new_view(|cx| {
6219 let mut item = TestItem::new(cx);
6220 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6221 item
6222 });
6223 workspace.update(cx, |workspace, cx| {
6224 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6225 });
6226 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6227 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6228
6229 // Adding an item that creates ambiguity increases the level of detail only
6230 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6231 // we stop at the highest detail available.
6232 let item3 = cx.new_view(|cx| {
6233 let mut item = TestItem::new(cx);
6234 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6235 item
6236 });
6237 workspace.update(cx, |workspace, cx| {
6238 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6239 });
6240 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6241 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6242 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6243 }
6244
6245 #[gpui::test]
6246 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6247 init_test(cx);
6248
6249 let fs = FakeFs::new(cx.executor());
6250 fs.insert_tree(
6251 "/root1",
6252 json!({
6253 "one.txt": "",
6254 "two.txt": "",
6255 }),
6256 )
6257 .await;
6258 fs.insert_tree(
6259 "/root2",
6260 json!({
6261 "three.txt": "",
6262 }),
6263 )
6264 .await;
6265
6266 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6267 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6268 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6269 let worktree_id = project.update(cx, |project, cx| {
6270 project.worktrees(cx).next().unwrap().read(cx).id()
6271 });
6272
6273 let item1 = cx.new_view(|cx| {
6274 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6275 });
6276 let item2 = cx.new_view(|cx| {
6277 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6278 });
6279
6280 // Add an item to an empty pane
6281 workspace.update(cx, |workspace, cx| {
6282 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6283 });
6284 project.update(cx, |project, cx| {
6285 assert_eq!(
6286 project.active_entry(),
6287 project
6288 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6289 .map(|e| e.id)
6290 );
6291 });
6292 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6293
6294 // Add a second item to a non-empty pane
6295 workspace.update(cx, |workspace, cx| {
6296 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6297 });
6298 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6299 project.update(cx, |project, cx| {
6300 assert_eq!(
6301 project.active_entry(),
6302 project
6303 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6304 .map(|e| e.id)
6305 );
6306 });
6307
6308 // Close the active item
6309 pane.update(cx, |pane, cx| {
6310 pane.close_active_item(&Default::default(), cx).unwrap()
6311 })
6312 .await
6313 .unwrap();
6314 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6315 project.update(cx, |project, cx| {
6316 assert_eq!(
6317 project.active_entry(),
6318 project
6319 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6320 .map(|e| e.id)
6321 );
6322 });
6323
6324 // Add a project folder
6325 project
6326 .update(cx, |project, cx| {
6327 project.find_or_create_worktree("root2", true, cx)
6328 })
6329 .await
6330 .unwrap();
6331 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6332
6333 // Remove a project folder
6334 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6335 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6336 }
6337
6338 #[gpui::test]
6339 async fn test_close_window(cx: &mut TestAppContext) {
6340 init_test(cx);
6341
6342 let fs = FakeFs::new(cx.executor());
6343 fs.insert_tree("/root", json!({ "one": "" })).await;
6344
6345 let project = Project::test(fs, ["root".as_ref()], cx).await;
6346 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6347
6348 // When there are no dirty items, there's nothing to do.
6349 let item1 = cx.new_view(TestItem::new);
6350 workspace.update(cx, |w, cx| {
6351 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6352 });
6353 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6354 assert!(task.await.unwrap());
6355
6356 // When there are dirty untitled items, prompt to save each one. If the user
6357 // cancels any prompt, then abort.
6358 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6359 let item3 = cx.new_view(|cx| {
6360 TestItem::new(cx)
6361 .with_dirty(true)
6362 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6363 });
6364 workspace.update(cx, |w, cx| {
6365 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6366 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6367 });
6368 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6369 cx.executor().run_until_parked();
6370 cx.simulate_prompt_answer(2); // cancel save all
6371 cx.executor().run_until_parked();
6372 cx.simulate_prompt_answer(2); // cancel save all
6373 cx.executor().run_until_parked();
6374 assert!(!cx.has_pending_prompt());
6375 assert!(!task.await.unwrap());
6376 }
6377
6378 #[gpui::test]
6379 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6380 init_test(cx);
6381
6382 // Register TestItem as a serializable item
6383 cx.update(|cx| {
6384 register_serializable_item::<TestItem>(cx);
6385 });
6386
6387 let fs = FakeFs::new(cx.executor());
6388 fs.insert_tree("/root", json!({ "one": "" })).await;
6389
6390 let project = Project::test(fs, ["root".as_ref()], cx).await;
6391 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6392
6393 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6394 let item1 = cx.new_view(|cx| {
6395 TestItem::new(cx)
6396 .with_dirty(true)
6397 .with_serialize(|| Some(Task::ready(Ok(()))))
6398 });
6399 let item2 = cx.new_view(|cx| {
6400 TestItem::new(cx)
6401 .with_dirty(true)
6402 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6403 .with_serialize(|| Some(Task::ready(Ok(()))))
6404 });
6405 workspace.update(cx, |w, cx| {
6406 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6407 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6408 });
6409 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6410 assert!(task.await.unwrap());
6411 }
6412
6413 #[gpui::test]
6414 async fn test_close_pane_items(cx: &mut TestAppContext) {
6415 init_test(cx);
6416
6417 let fs = FakeFs::new(cx.executor());
6418
6419 let project = Project::test(fs, None, cx).await;
6420 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6421
6422 let item1 = cx.new_view(|cx| {
6423 TestItem::new(cx)
6424 .with_dirty(true)
6425 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6426 });
6427 let item2 = cx.new_view(|cx| {
6428 TestItem::new(cx)
6429 .with_dirty(true)
6430 .with_conflict(true)
6431 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6432 });
6433 let item3 = cx.new_view(|cx| {
6434 TestItem::new(cx)
6435 .with_dirty(true)
6436 .with_conflict(true)
6437 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6438 });
6439 let item4 = cx.new_view(|cx| {
6440 TestItem::new(cx).with_dirty(true).with_project_items(&[{
6441 let project_item = TestProjectItem::new_untitled(cx);
6442 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6443 project_item
6444 }])
6445 });
6446 let pane = workspace.update(cx, |workspace, cx| {
6447 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6448 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6449 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6450 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6451 workspace.active_pane().clone()
6452 });
6453
6454 let close_items = pane.update(cx, |pane, cx| {
6455 pane.activate_item(1, true, true, cx);
6456 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6457 let item1_id = item1.item_id();
6458 let item3_id = item3.item_id();
6459 let item4_id = item4.item_id();
6460 pane.close_items(cx, SaveIntent::Close, move |id| {
6461 [item1_id, item3_id, item4_id].contains(&id)
6462 })
6463 });
6464 cx.executor().run_until_parked();
6465
6466 assert!(cx.has_pending_prompt());
6467 // Ignore "Save all" prompt
6468 cx.simulate_prompt_answer(2);
6469 cx.executor().run_until_parked();
6470 // There's a prompt to save item 1.
6471 pane.update(cx, |pane, _| {
6472 assert_eq!(pane.items_len(), 4);
6473 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6474 });
6475 // Confirm saving item 1.
6476 cx.simulate_prompt_answer(0);
6477 cx.executor().run_until_parked();
6478
6479 // Item 1 is saved. There's a prompt to save item 3.
6480 pane.update(cx, |pane, cx| {
6481 assert_eq!(item1.read(cx).save_count, 1);
6482 assert_eq!(item1.read(cx).save_as_count, 0);
6483 assert_eq!(item1.read(cx).reload_count, 0);
6484 assert_eq!(pane.items_len(), 3);
6485 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6486 });
6487 assert!(cx.has_pending_prompt());
6488
6489 // Cancel saving item 3.
6490 cx.simulate_prompt_answer(1);
6491 cx.executor().run_until_parked();
6492
6493 // Item 3 is reloaded. There's a prompt to save item 4.
6494 pane.update(cx, |pane, cx| {
6495 assert_eq!(item3.read(cx).save_count, 0);
6496 assert_eq!(item3.read(cx).save_as_count, 0);
6497 assert_eq!(item3.read(cx).reload_count, 1);
6498 assert_eq!(pane.items_len(), 2);
6499 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6500 });
6501 assert!(cx.has_pending_prompt());
6502
6503 // Confirm saving item 4.
6504 cx.simulate_prompt_answer(0);
6505 cx.executor().run_until_parked();
6506
6507 // There's a prompt for a path for item 4.
6508 cx.simulate_new_path_selection(|_| Some(Default::default()));
6509 close_items.await.unwrap();
6510
6511 // The requested items are closed.
6512 pane.update(cx, |pane, cx| {
6513 assert_eq!(item4.read(cx).save_count, 0);
6514 assert_eq!(item4.read(cx).save_as_count, 1);
6515 assert_eq!(item4.read(cx).reload_count, 0);
6516 assert_eq!(pane.items_len(), 1);
6517 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6518 });
6519 }
6520
6521 #[gpui::test]
6522 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6523 init_test(cx);
6524
6525 let fs = FakeFs::new(cx.executor());
6526 let project = Project::test(fs, [], cx).await;
6527 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6528
6529 // Create several workspace items with single project entries, and two
6530 // workspace items with multiple project entries.
6531 let single_entry_items = (0..=4)
6532 .map(|project_entry_id| {
6533 cx.new_view(|cx| {
6534 TestItem::new(cx)
6535 .with_dirty(true)
6536 .with_project_items(&[dirty_project_item(
6537 project_entry_id,
6538 &format!("{project_entry_id}.txt"),
6539 cx,
6540 )])
6541 })
6542 })
6543 .collect::<Vec<_>>();
6544 let item_2_3 = cx.new_view(|cx| {
6545 TestItem::new(cx)
6546 .with_dirty(true)
6547 .with_singleton(false)
6548 .with_project_items(&[
6549 single_entry_items[2].read(cx).project_items[0].clone(),
6550 single_entry_items[3].read(cx).project_items[0].clone(),
6551 ])
6552 });
6553 let item_3_4 = cx.new_view(|cx| {
6554 TestItem::new(cx)
6555 .with_dirty(true)
6556 .with_singleton(false)
6557 .with_project_items(&[
6558 single_entry_items[3].read(cx).project_items[0].clone(),
6559 single_entry_items[4].read(cx).project_items[0].clone(),
6560 ])
6561 });
6562
6563 // Create two panes that contain the following project entries:
6564 // left pane:
6565 // multi-entry items: (2, 3)
6566 // single-entry items: 0, 1, 2, 3, 4
6567 // right pane:
6568 // single-entry items: 1
6569 // multi-entry items: (3, 4)
6570 let left_pane = workspace.update(cx, |workspace, cx| {
6571 let left_pane = workspace.active_pane().clone();
6572 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6573 for item in single_entry_items {
6574 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6575 }
6576 left_pane.update(cx, |pane, cx| {
6577 pane.activate_item(2, true, true, cx);
6578 });
6579
6580 let right_pane = workspace
6581 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6582 .unwrap();
6583
6584 right_pane.update(cx, |pane, cx| {
6585 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6586 });
6587
6588 left_pane
6589 });
6590
6591 cx.focus_view(&left_pane);
6592
6593 // When closing all of the items in the left pane, we should be prompted twice:
6594 // once for project entry 0, and once for project entry 2. Project entries 1,
6595 // 3, and 4 are all still open in the other paten. After those two
6596 // prompts, the task should complete.
6597
6598 let close = left_pane.update(cx, |pane, cx| {
6599 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6600 });
6601 cx.executor().run_until_parked();
6602
6603 // Discard "Save all" prompt
6604 cx.simulate_prompt_answer(2);
6605
6606 cx.executor().run_until_parked();
6607 left_pane.update(cx, |pane, cx| {
6608 assert_eq!(
6609 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6610 &[ProjectEntryId::from_proto(0)]
6611 );
6612 });
6613 cx.simulate_prompt_answer(0);
6614
6615 cx.executor().run_until_parked();
6616 left_pane.update(cx, |pane, cx| {
6617 assert_eq!(
6618 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6619 &[ProjectEntryId::from_proto(2)]
6620 );
6621 });
6622 cx.simulate_prompt_answer(0);
6623
6624 cx.executor().run_until_parked();
6625 close.await.unwrap();
6626 left_pane.update(cx, |pane, _| {
6627 assert_eq!(pane.items_len(), 0);
6628 });
6629 }
6630
6631 #[gpui::test]
6632 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6633 init_test(cx);
6634
6635 let fs = FakeFs::new(cx.executor());
6636 let project = Project::test(fs, [], cx).await;
6637 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6638 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6639
6640 let item = cx.new_view(|cx| {
6641 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6642 });
6643 let item_id = item.entity_id();
6644 workspace.update(cx, |workspace, cx| {
6645 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6646 });
6647
6648 // Autosave on window change.
6649 item.update(cx, |item, cx| {
6650 SettingsStore::update_global(cx, |settings, cx| {
6651 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6652 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6653 })
6654 });
6655 item.is_dirty = true;
6656 });
6657
6658 // Deactivating the window saves the file.
6659 cx.deactivate_window();
6660 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6661
6662 // Re-activating the window doesn't save the file.
6663 cx.update(|cx| cx.activate_window());
6664 cx.executor().run_until_parked();
6665 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6666
6667 // Autosave on focus change.
6668 item.update(cx, |item, cx| {
6669 cx.focus_self();
6670 SettingsStore::update_global(cx, |settings, cx| {
6671 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6672 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6673 })
6674 });
6675 item.is_dirty = true;
6676 });
6677
6678 // Blurring the item saves the file.
6679 item.update(cx, |_, cx| cx.blur());
6680 cx.executor().run_until_parked();
6681 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6682
6683 // Deactivating the window still saves the file.
6684 item.update(cx, |item, cx| {
6685 cx.focus_self();
6686 item.is_dirty = true;
6687 });
6688 cx.deactivate_window();
6689 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6690
6691 // Autosave after delay.
6692 item.update(cx, |item, cx| {
6693 SettingsStore::update_global(cx, |settings, cx| {
6694 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6695 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6696 })
6697 });
6698 item.is_dirty = true;
6699 cx.emit(ItemEvent::Edit);
6700 });
6701
6702 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6703 cx.executor().advance_clock(Duration::from_millis(250));
6704 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6705
6706 // After delay expires, the file is saved.
6707 cx.executor().advance_clock(Duration::from_millis(250));
6708 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6709
6710 // Autosave on focus change, ensuring closing the tab counts as such.
6711 item.update(cx, |item, cx| {
6712 SettingsStore::update_global(cx, |settings, cx| {
6713 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6714 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6715 })
6716 });
6717 item.is_dirty = true;
6718 for project_item in &mut item.project_items {
6719 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6720 }
6721 });
6722
6723 pane.update(cx, |pane, cx| {
6724 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6725 })
6726 .await
6727 .unwrap();
6728 assert!(!cx.has_pending_prompt());
6729 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6730
6731 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6732 workspace.update(cx, |workspace, cx| {
6733 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6734 });
6735 item.update(cx, |item, cx| {
6736 item.project_items[0].update(cx, |item, _| {
6737 item.entry_id = None;
6738 });
6739 item.is_dirty = true;
6740 cx.blur();
6741 });
6742 cx.run_until_parked();
6743 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6744
6745 // Ensure autosave is prevented for deleted files also when closing the buffer.
6746 let _close_items = pane.update(cx, |pane, cx| {
6747 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6748 });
6749 cx.run_until_parked();
6750 assert!(cx.has_pending_prompt());
6751 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6752 }
6753
6754 #[gpui::test]
6755 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6756 init_test(cx);
6757
6758 let fs = FakeFs::new(cx.executor());
6759
6760 let project = Project::test(fs, [], cx).await;
6761 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6762
6763 let item = cx.new_view(|cx| {
6764 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6765 });
6766 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6767 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6768 let toolbar_notify_count = Rc::new(RefCell::new(0));
6769
6770 workspace.update(cx, |workspace, cx| {
6771 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6772 let toolbar_notification_count = toolbar_notify_count.clone();
6773 cx.observe(&toolbar, move |_, _, _| {
6774 *toolbar_notification_count.borrow_mut() += 1
6775 })
6776 .detach();
6777 });
6778
6779 pane.update(cx, |pane, _| {
6780 assert!(!pane.can_navigate_backward());
6781 assert!(!pane.can_navigate_forward());
6782 });
6783
6784 item.update(cx, |item, cx| {
6785 item.set_state("one".to_string(), cx);
6786 });
6787
6788 // Toolbar must be notified to re-render the navigation buttons
6789 assert_eq!(*toolbar_notify_count.borrow(), 1);
6790
6791 pane.update(cx, |pane, _| {
6792 assert!(pane.can_navigate_backward());
6793 assert!(!pane.can_navigate_forward());
6794 });
6795
6796 workspace
6797 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6798 .await
6799 .unwrap();
6800
6801 assert_eq!(*toolbar_notify_count.borrow(), 2);
6802 pane.update(cx, |pane, _| {
6803 assert!(!pane.can_navigate_backward());
6804 assert!(pane.can_navigate_forward());
6805 });
6806 }
6807
6808 #[gpui::test]
6809 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6810 init_test(cx);
6811 let fs = FakeFs::new(cx.executor());
6812
6813 let project = Project::test(fs, [], cx).await;
6814 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6815
6816 let panel = workspace.update(cx, |workspace, cx| {
6817 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6818 workspace.add_panel(panel.clone(), cx);
6819
6820 workspace
6821 .right_dock()
6822 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6823
6824 panel
6825 });
6826
6827 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6828 pane.update(cx, |pane, cx| {
6829 let item = cx.new_view(TestItem::new);
6830 pane.add_item(Box::new(item), true, true, None, cx);
6831 });
6832
6833 // Transfer focus from center to panel
6834 workspace.update(cx, |workspace, cx| {
6835 workspace.toggle_panel_focus::<TestPanel>(cx);
6836 });
6837
6838 workspace.update(cx, |workspace, cx| {
6839 assert!(workspace.right_dock().read(cx).is_open());
6840 assert!(!panel.is_zoomed(cx));
6841 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6842 });
6843
6844 // Transfer focus from panel to center
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 // Close the dock
6856 workspace.update(cx, |workspace, cx| {
6857 workspace.toggle_dock(DockPosition::Right, 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 // Open 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 // Focus and zoom panel
6878 panel.update(cx, |panel, cx| {
6879 cx.focus_self();
6880 panel.set_zoomed(true, cx)
6881 });
6882
6883 workspace.update(cx, |workspace, cx| {
6884 assert!(workspace.right_dock().read(cx).is_open());
6885 assert!(panel.is_zoomed(cx));
6886 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6887 });
6888
6889 // Transfer focus to the center closes the dock
6890 workspace.update(cx, |workspace, cx| {
6891 workspace.toggle_panel_focus::<TestPanel>(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 // Transferring focus back to the panel keeps it zoomed
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 // Close the dock while it is zoomed
6912 workspace.update(cx, |workspace, cx| {
6913 workspace.toggle_dock(DockPosition::Right, 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!(workspace.zoomed.is_none());
6920 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6921 });
6922
6923 // Opening the dock, when it's zoomed, retains focus
6924 workspace.update(cx, |workspace, cx| {
6925 workspace.toggle_dock(DockPosition::Right, cx)
6926 });
6927
6928 workspace.update(cx, |workspace, cx| {
6929 assert!(workspace.right_dock().read(cx).is_open());
6930 assert!(panel.is_zoomed(cx));
6931 assert!(workspace.zoomed.is_some());
6932 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6933 });
6934
6935 // Unzoom and close the panel, zoom the active pane.
6936 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6937 workspace.update(cx, |workspace, cx| {
6938 workspace.toggle_dock(DockPosition::Right, cx)
6939 });
6940 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6941
6942 // Opening a dock unzooms the pane.
6943 workspace.update(cx, |workspace, cx| {
6944 workspace.toggle_dock(DockPosition::Right, cx)
6945 });
6946 workspace.update(cx, |workspace, cx| {
6947 let pane = pane.read(cx);
6948 assert!(!pane.is_zoomed());
6949 assert!(!pane.focus_handle(cx).is_focused(cx));
6950 assert!(workspace.right_dock().read(cx).is_open());
6951 assert!(workspace.zoomed.is_none());
6952 });
6953 }
6954
6955 #[gpui::test]
6956 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6957 init_test(cx);
6958
6959 let fs = FakeFs::new(cx.executor());
6960
6961 let project = Project::test(fs, None, cx).await;
6962 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6963
6964 // Let's arrange the panes like this:
6965 //
6966 // +-----------------------+
6967 // | top |
6968 // +------+--------+-------+
6969 // | left | center | right |
6970 // +------+--------+-------+
6971 // | bottom |
6972 // +-----------------------+
6973
6974 let top_item = cx.new_view(|cx| {
6975 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6976 });
6977 let bottom_item = cx.new_view(|cx| {
6978 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6979 });
6980 let left_item = cx.new_view(|cx| {
6981 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6982 });
6983 let right_item = cx.new_view(|cx| {
6984 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6985 });
6986 let center_item = cx.new_view(|cx| {
6987 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6988 });
6989
6990 let top_pane_id = workspace.update(cx, |workspace, cx| {
6991 let top_pane_id = workspace.active_pane().entity_id();
6992 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
6993 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
6994 top_pane_id
6995 });
6996 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
6997 let bottom_pane_id = workspace.active_pane().entity_id();
6998 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
6999 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
7000 bottom_pane_id
7001 });
7002 let left_pane_id = workspace.update(cx, |workspace, cx| {
7003 let left_pane_id = workspace.active_pane().entity_id();
7004 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
7005 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7006 left_pane_id
7007 });
7008 let right_pane_id = workspace.update(cx, |workspace, cx| {
7009 let right_pane_id = workspace.active_pane().entity_id();
7010 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
7011 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
7012 right_pane_id
7013 });
7014 let center_pane_id = workspace.update(cx, |workspace, cx| {
7015 let center_pane_id = workspace.active_pane().entity_id();
7016 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
7017 center_pane_id
7018 });
7019 cx.executor().run_until_parked();
7020
7021 workspace.update(cx, |workspace, cx| {
7022 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7023
7024 // Join into next from center pane into right
7025 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7026 });
7027
7028 workspace.update(cx, |workspace, cx| {
7029 let active_pane = workspace.active_pane();
7030 assert_eq!(right_pane_id, active_pane.entity_id());
7031 assert_eq!(2, active_pane.read(cx).items_len());
7032 let item_ids_in_pane =
7033 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7034 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7035 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7036
7037 // Join into next from right pane into bottom
7038 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7039 });
7040
7041 workspace.update(cx, |workspace, cx| {
7042 let active_pane = workspace.active_pane();
7043 assert_eq!(bottom_pane_id, active_pane.entity_id());
7044 assert_eq!(3, active_pane.read(cx).items_len());
7045 let item_ids_in_pane =
7046 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7047 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7048 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7049 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7050
7051 // Join into next from bottom pane into left
7052 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7053 });
7054
7055 workspace.update(cx, |workspace, cx| {
7056 let active_pane = workspace.active_pane();
7057 assert_eq!(left_pane_id, active_pane.entity_id());
7058 assert_eq!(4, active_pane.read(cx).items_len());
7059 let item_ids_in_pane =
7060 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7061 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7062 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7063 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7064 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7065
7066 // Join into next from left pane into top
7067 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7068 });
7069
7070 workspace.update(cx, |workspace, cx| {
7071 let active_pane = workspace.active_pane();
7072 assert_eq!(top_pane_id, active_pane.entity_id());
7073 assert_eq!(5, active_pane.read(cx).items_len());
7074 let item_ids_in_pane =
7075 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7076 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7077 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7078 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7079 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7080 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7081
7082 // Single pane left: no-op
7083 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7084 });
7085
7086 workspace.update(cx, |workspace, _cx| {
7087 let active_pane = workspace.active_pane();
7088 assert_eq!(top_pane_id, active_pane.entity_id());
7089 });
7090 }
7091
7092 fn add_an_item_to_active_pane(
7093 cx: &mut VisualTestContext,
7094 workspace: &View<Workspace>,
7095 item_id: u64,
7096 ) -> View<TestItem> {
7097 let item = cx.new_view(|cx| {
7098 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7099 item_id,
7100 "item{item_id}.txt",
7101 cx,
7102 )])
7103 });
7104 workspace.update(cx, |workspace, cx| {
7105 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7106 });
7107 return item;
7108 }
7109
7110 fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7111 return workspace.update(cx, |workspace, cx| {
7112 let new_pane =
7113 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7114 new_pane
7115 });
7116 }
7117
7118 #[gpui::test]
7119 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7120 init_test(cx);
7121 let fs = FakeFs::new(cx.executor());
7122 let project = Project::test(fs, None, cx).await;
7123 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7124
7125 add_an_item_to_active_pane(cx, &workspace, 1);
7126 split_pane(cx, &workspace);
7127 add_an_item_to_active_pane(cx, &workspace, 2);
7128 split_pane(cx, &workspace); // empty pane
7129 split_pane(cx, &workspace);
7130 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7131
7132 cx.executor().run_until_parked();
7133
7134 workspace.update(cx, |workspace, cx| {
7135 let num_panes = workspace.panes().len();
7136 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7137 let active_item = workspace
7138 .active_pane()
7139 .read(cx)
7140 .active_item()
7141 .expect("item is in focus");
7142
7143 assert_eq!(num_panes, 4);
7144 assert_eq!(num_items_in_current_pane, 1);
7145 assert_eq!(active_item.item_id(), last_item.item_id());
7146 });
7147
7148 workspace.update(cx, |workspace, cx| {
7149 workspace.join_all_panes(cx);
7150 });
7151
7152 workspace.update(cx, |workspace, cx| {
7153 let num_panes = workspace.panes().len();
7154 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7155 let active_item = workspace
7156 .active_pane()
7157 .read(cx)
7158 .active_item()
7159 .expect("item is in focus");
7160
7161 assert_eq!(num_panes, 1);
7162 assert_eq!(num_items_in_current_pane, 3);
7163 assert_eq!(active_item.item_id(), last_item.item_id());
7164 });
7165 }
7166 struct TestModal(FocusHandle);
7167
7168 impl TestModal {
7169 fn new(cx: &mut ViewContext<Self>) -> Self {
7170 Self(cx.focus_handle())
7171 }
7172 }
7173
7174 impl EventEmitter<DismissEvent> for TestModal {}
7175
7176 impl FocusableView for TestModal {
7177 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7178 self.0.clone()
7179 }
7180 }
7181
7182 impl ModalView for TestModal {}
7183
7184 impl Render for TestModal {
7185 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7186 div().track_focus(&self.0)
7187 }
7188 }
7189
7190 #[gpui::test]
7191 async fn test_panels(cx: &mut gpui::TestAppContext) {
7192 init_test(cx);
7193 let fs = FakeFs::new(cx.executor());
7194
7195 let project = Project::test(fs, [], cx).await;
7196 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7197
7198 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7199 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7200 workspace.add_panel(panel_1.clone(), cx);
7201 workspace
7202 .left_dock()
7203 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7204 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7205 workspace.add_panel(panel_2.clone(), cx);
7206 workspace
7207 .right_dock()
7208 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7209
7210 let left_dock = workspace.left_dock();
7211 assert_eq!(
7212 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7213 panel_1.panel_id()
7214 );
7215 assert_eq!(
7216 left_dock.read(cx).active_panel_size(cx).unwrap(),
7217 panel_1.size(cx)
7218 );
7219
7220 left_dock.update(cx, |left_dock, cx| {
7221 left_dock.resize_active_panel(Some(px(1337.)), cx)
7222 });
7223 assert_eq!(
7224 workspace
7225 .right_dock()
7226 .read(cx)
7227 .visible_panel()
7228 .unwrap()
7229 .panel_id(),
7230 panel_2.panel_id(),
7231 );
7232
7233 (panel_1, panel_2)
7234 });
7235
7236 // Move panel_1 to the right
7237 panel_1.update(cx, |panel_1, cx| {
7238 panel_1.set_position(DockPosition::Right, cx)
7239 });
7240
7241 workspace.update(cx, |workspace, cx| {
7242 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7243 // Since it was the only panel on the left, the left dock should now be closed.
7244 assert!(!workspace.left_dock().read(cx).is_open());
7245 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7246 let right_dock = workspace.right_dock();
7247 assert_eq!(
7248 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7249 panel_1.panel_id()
7250 );
7251 assert_eq!(
7252 right_dock.read(cx).active_panel_size(cx).unwrap(),
7253 px(1337.)
7254 );
7255
7256 // Now we move panel_2 to the left
7257 panel_2.set_position(DockPosition::Left, cx);
7258 });
7259
7260 workspace.update(cx, |workspace, cx| {
7261 // Since panel_2 was not visible on the right, we don't open the left dock.
7262 assert!(!workspace.left_dock().read(cx).is_open());
7263 // And the right dock is unaffected in its displaying of panel_1
7264 assert!(workspace.right_dock().read(cx).is_open());
7265 assert_eq!(
7266 workspace
7267 .right_dock()
7268 .read(cx)
7269 .visible_panel()
7270 .unwrap()
7271 .panel_id(),
7272 panel_1.panel_id(),
7273 );
7274 });
7275
7276 // Move panel_1 back to the left
7277 panel_1.update(cx, |panel_1, cx| {
7278 panel_1.set_position(DockPosition::Left, cx)
7279 });
7280
7281 workspace.update(cx, |workspace, cx| {
7282 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7283 let left_dock = workspace.left_dock();
7284 assert!(left_dock.read(cx).is_open());
7285 assert_eq!(
7286 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7287 panel_1.panel_id()
7288 );
7289 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7290 // And the right dock should be closed as it no longer has any panels.
7291 assert!(!workspace.right_dock().read(cx).is_open());
7292
7293 // Now we move panel_1 to the bottom
7294 panel_1.set_position(DockPosition::Bottom, cx);
7295 });
7296
7297 workspace.update(cx, |workspace, cx| {
7298 // Since panel_1 was visible on the left, we close the left dock.
7299 assert!(!workspace.left_dock().read(cx).is_open());
7300 // The bottom dock is sized based on the panel's default size,
7301 // since the panel orientation changed from vertical to horizontal.
7302 let bottom_dock = workspace.bottom_dock();
7303 assert_eq!(
7304 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7305 panel_1.size(cx),
7306 );
7307 // Close bottom dock and move panel_1 back to the left.
7308 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7309 panel_1.set_position(DockPosition::Left, cx);
7310 });
7311
7312 // Emit activated event on panel 1
7313 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7314
7315 // Now the left dock is open and panel_1 is active and focused.
7316 workspace.update(cx, |workspace, cx| {
7317 let left_dock = workspace.left_dock();
7318 assert!(left_dock.read(cx).is_open());
7319 assert_eq!(
7320 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7321 panel_1.panel_id(),
7322 );
7323 assert!(panel_1.focus_handle(cx).is_focused(cx));
7324 });
7325
7326 // Emit closed event on panel 2, which is not active
7327 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7328
7329 // Wo don't close the left dock, because panel_2 wasn't the active panel
7330 workspace.update(cx, |workspace, cx| {
7331 let left_dock = workspace.left_dock();
7332 assert!(left_dock.read(cx).is_open());
7333 assert_eq!(
7334 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7335 panel_1.panel_id(),
7336 );
7337 });
7338
7339 // Emitting a ZoomIn event shows the panel as zoomed.
7340 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7341 workspace.update(cx, |workspace, _| {
7342 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7343 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7344 });
7345
7346 // Move panel to another dock while it is zoomed
7347 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7348 workspace.update(cx, |workspace, _| {
7349 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7350
7351 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7352 });
7353
7354 // This is a helper for getting a:
7355 // - valid focus on an element,
7356 // - that isn't a part of the panes and panels system of the Workspace,
7357 // - and doesn't trigger the 'on_focus_lost' API.
7358 let focus_other_view = {
7359 let workspace = workspace.clone();
7360 move |cx: &mut VisualTestContext| {
7361 workspace.update(cx, |workspace, cx| {
7362 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7363 workspace.toggle_modal(cx, TestModal::new);
7364 workspace.toggle_modal(cx, TestModal::new);
7365 } else {
7366 workspace.toggle_modal(cx, TestModal::new);
7367 }
7368 })
7369 }
7370 };
7371
7372 // If focus is transferred to another view that's not a panel or another pane, we still show
7373 // the panel as zoomed.
7374 focus_other_view(cx);
7375 workspace.update(cx, |workspace, _| {
7376 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7377 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7378 });
7379
7380 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7381 workspace.update(cx, |_, cx| cx.focus_self());
7382 workspace.update(cx, |workspace, _| {
7383 assert_eq!(workspace.zoomed, None);
7384 assert_eq!(workspace.zoomed_position, None);
7385 });
7386
7387 // If focus is transferred again to another view that's not a panel or a pane, we won't
7388 // show the panel as zoomed because it wasn't zoomed before.
7389 focus_other_view(cx);
7390 workspace.update(cx, |workspace, _| {
7391 assert_eq!(workspace.zoomed, None);
7392 assert_eq!(workspace.zoomed_position, None);
7393 });
7394
7395 // When the panel is activated, it is zoomed again.
7396 cx.dispatch_action(ToggleRightDock);
7397 workspace.update(cx, |workspace, _| {
7398 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7399 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7400 });
7401
7402 // Emitting a ZoomOut event unzooms the panel.
7403 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7404 workspace.update(cx, |workspace, _| {
7405 assert_eq!(workspace.zoomed, None);
7406 assert_eq!(workspace.zoomed_position, None);
7407 });
7408
7409 // Emit closed event on panel 1, which is active
7410 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7411
7412 // Now the left dock is closed, because panel_1 was the active panel
7413 workspace.update(cx, |workspace, cx| {
7414 let right_dock = workspace.right_dock();
7415 assert!(!right_dock.read(cx).is_open());
7416 });
7417 }
7418
7419 #[gpui::test]
7420 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
7421 init_test(cx);
7422
7423 let fs = FakeFs::new(cx.background_executor.clone());
7424 let project = Project::test(fs, [], cx).await;
7425 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7426 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7427
7428 let dirty_regular_buffer = cx.new_view(|cx| {
7429 TestItem::new(cx)
7430 .with_dirty(true)
7431 .with_label("1.txt")
7432 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7433 });
7434 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7435 TestItem::new(cx)
7436 .with_dirty(true)
7437 .with_label("2.txt")
7438 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7439 });
7440 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7441 TestItem::new(cx)
7442 .with_dirty(true)
7443 .with_singleton(false)
7444 .with_label("Fake Project Search")
7445 .with_project_items(&[
7446 dirty_regular_buffer.read(cx).project_items[0].clone(),
7447 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7448 ])
7449 });
7450 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7451 workspace.update(cx, |workspace, cx| {
7452 workspace.add_item(
7453 pane.clone(),
7454 Box::new(dirty_regular_buffer.clone()),
7455 None,
7456 false,
7457 false,
7458 cx,
7459 );
7460 workspace.add_item(
7461 pane.clone(),
7462 Box::new(dirty_regular_buffer_2.clone()),
7463 None,
7464 false,
7465 false,
7466 cx,
7467 );
7468 workspace.add_item(
7469 pane.clone(),
7470 Box::new(dirty_multi_buffer_with_both.clone()),
7471 None,
7472 false,
7473 false,
7474 cx,
7475 );
7476 });
7477
7478 pane.update(cx, |pane, cx| {
7479 pane.activate_item(2, true, true, cx);
7480 assert_eq!(
7481 pane.active_item().unwrap().item_id(),
7482 multi_buffer_with_both_files_id,
7483 "Should select the multi buffer in the pane"
7484 );
7485 });
7486 let close_all_but_multi_buffer_task = pane
7487 .update(cx, |pane, cx| {
7488 pane.close_inactive_items(
7489 &CloseInactiveItems {
7490 save_intent: Some(SaveIntent::Save),
7491 close_pinned: true,
7492 },
7493 cx,
7494 )
7495 })
7496 .expect("should have inactive files to close");
7497 cx.background_executor.run_until_parked();
7498 assert!(
7499 !cx.has_pending_prompt(),
7500 "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
7501 );
7502 close_all_but_multi_buffer_task
7503 .await
7504 .expect("Closing all buffers but the multi buffer failed");
7505 pane.update(cx, |pane, cx| {
7506 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7507 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
7508 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7509 assert_eq!(pane.items_len(), 1);
7510 assert_eq!(
7511 pane.active_item().unwrap().item_id(),
7512 multi_buffer_with_both_files_id,
7513 "Should have only the multi buffer left in the pane"
7514 );
7515 assert!(
7516 dirty_multi_buffer_with_both.read(cx).is_dirty,
7517 "The multi buffer containing the unsaved buffer should still be dirty"
7518 );
7519 });
7520
7521 let close_multi_buffer_task = pane
7522 .update(cx, |pane, cx| {
7523 pane.close_active_item(
7524 &CloseActiveItem {
7525 save_intent: Some(SaveIntent::Close),
7526 },
7527 cx,
7528 )
7529 })
7530 .expect("should have the multi buffer to close");
7531 cx.background_executor.run_until_parked();
7532 assert!(
7533 cx.has_pending_prompt(),
7534 "Dirty multi buffer should prompt a save dialog"
7535 );
7536 cx.simulate_prompt_answer(0);
7537 cx.background_executor.run_until_parked();
7538 close_multi_buffer_task
7539 .await
7540 .expect("Closing the multi buffer failed");
7541 pane.update(cx, |pane, cx| {
7542 assert_eq!(
7543 dirty_multi_buffer_with_both.read(cx).save_count,
7544 1,
7545 "Multi buffer item should get be saved"
7546 );
7547 // Test impl does not save inner items, so we do not assert them
7548 assert_eq!(
7549 pane.items_len(),
7550 0,
7551 "No more items should be left in the pane"
7552 );
7553 assert!(pane.active_item().is_none());
7554 });
7555 }
7556
7557 #[gpui::test]
7558 async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
7559 cx: &mut TestAppContext,
7560 ) {
7561 init_test(cx);
7562
7563 let fs = FakeFs::new(cx.background_executor.clone());
7564 let project = Project::test(fs, [], cx).await;
7565 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7566 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7567
7568 let dirty_regular_buffer = cx.new_view(|cx| {
7569 TestItem::new(cx)
7570 .with_dirty(true)
7571 .with_label("1.txt")
7572 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7573 });
7574 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7575 TestItem::new(cx)
7576 .with_dirty(true)
7577 .with_label("2.txt")
7578 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7579 });
7580 let clear_regular_buffer = cx.new_view(|cx| {
7581 TestItem::new(cx)
7582 .with_label("3.txt")
7583 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7584 });
7585
7586 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7587 TestItem::new(cx)
7588 .with_dirty(true)
7589 .with_singleton(false)
7590 .with_label("Fake Project Search")
7591 .with_project_items(&[
7592 dirty_regular_buffer.read(cx).project_items[0].clone(),
7593 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7594 clear_regular_buffer.read(cx).project_items[0].clone(),
7595 ])
7596 });
7597 workspace.update(cx, |workspace, cx| {
7598 workspace.add_item(
7599 pane.clone(),
7600 Box::new(dirty_regular_buffer.clone()),
7601 None,
7602 false,
7603 false,
7604 cx,
7605 );
7606 workspace.add_item(
7607 pane.clone(),
7608 Box::new(dirty_multi_buffer_with_both.clone()),
7609 None,
7610 false,
7611 false,
7612 cx,
7613 );
7614 });
7615
7616 pane.update(cx, |pane, cx| {
7617 pane.activate_item(0, true, true, cx);
7618 assert_eq!(
7619 pane.active_item().unwrap().item_id(),
7620 dirty_regular_buffer.item_id(),
7621 "Should select the dirty singleton buffer in the pane"
7622 );
7623 });
7624 let close_singleton_buffer_task = pane
7625 .update(cx, |pane, cx| {
7626 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7627 })
7628 .expect("should have active singleton buffer to close");
7629 cx.background_executor.run_until_parked();
7630 assert!(
7631 !cx.has_pending_prompt(),
7632 "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
7633 );
7634
7635 close_singleton_buffer_task
7636 .await
7637 .expect("Should not fail closing the singleton buffer");
7638 pane.update(cx, |pane, cx| {
7639 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7640 assert_eq!(
7641 dirty_multi_buffer_with_both.read(cx).save_count,
7642 0,
7643 "Multi buffer itself should not be saved"
7644 );
7645 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7646 assert_eq!(
7647 pane.items_len(),
7648 1,
7649 "A dirty multi buffer should be present in the pane"
7650 );
7651 assert_eq!(
7652 pane.active_item().unwrap().item_id(),
7653 dirty_multi_buffer_with_both.item_id(),
7654 "Should activate the only remaining item in the pane"
7655 );
7656 });
7657 }
7658
7659 #[gpui::test]
7660 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
7661 cx: &mut TestAppContext,
7662 ) {
7663 init_test(cx);
7664
7665 let fs = FakeFs::new(cx.background_executor.clone());
7666 let project = Project::test(fs, [], cx).await;
7667 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7668 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7669
7670 let dirty_regular_buffer = cx.new_view(|cx| {
7671 TestItem::new(cx)
7672 .with_dirty(true)
7673 .with_label("1.txt")
7674 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7675 });
7676 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7677 TestItem::new(cx)
7678 .with_dirty(true)
7679 .with_label("2.txt")
7680 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7681 });
7682 let clear_regular_buffer = cx.new_view(|cx| {
7683 TestItem::new(cx)
7684 .with_label("3.txt")
7685 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7686 });
7687
7688 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7689 TestItem::new(cx)
7690 .with_dirty(true)
7691 .with_singleton(false)
7692 .with_label("Fake Project Search")
7693 .with_project_items(&[
7694 dirty_regular_buffer.read(cx).project_items[0].clone(),
7695 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7696 clear_regular_buffer.read(cx).project_items[0].clone(),
7697 ])
7698 });
7699 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7700 workspace.update(cx, |workspace, cx| {
7701 workspace.add_item(
7702 pane.clone(),
7703 Box::new(dirty_regular_buffer.clone()),
7704 None,
7705 false,
7706 false,
7707 cx,
7708 );
7709 workspace.add_item(
7710 pane.clone(),
7711 Box::new(dirty_multi_buffer_with_both.clone()),
7712 None,
7713 false,
7714 false,
7715 cx,
7716 );
7717 });
7718
7719 pane.update(cx, |pane, cx| {
7720 pane.activate_item(1, true, true, cx);
7721 assert_eq!(
7722 pane.active_item().unwrap().item_id(),
7723 multi_buffer_with_both_files_id,
7724 "Should select the multi buffer in the pane"
7725 );
7726 });
7727 let _close_multi_buffer_task = pane
7728 .update(cx, |pane, cx| {
7729 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7730 })
7731 .expect("should have active multi buffer to close");
7732 cx.background_executor.run_until_parked();
7733 assert!(
7734 cx.has_pending_prompt(),
7735 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
7736 );
7737 }
7738
7739 #[gpui::test]
7740 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
7741 cx: &mut TestAppContext,
7742 ) {
7743 init_test(cx);
7744
7745 let fs = FakeFs::new(cx.background_executor.clone());
7746 let project = Project::test(fs, [], cx).await;
7747 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7748 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7749
7750 let dirty_regular_buffer = cx.new_view(|cx| {
7751 TestItem::new(cx)
7752 .with_dirty(true)
7753 .with_label("1.txt")
7754 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7755 });
7756 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7757 TestItem::new(cx)
7758 .with_dirty(true)
7759 .with_label("2.txt")
7760 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7761 });
7762 let clear_regular_buffer = cx.new_view(|cx| {
7763 TestItem::new(cx)
7764 .with_label("3.txt")
7765 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7766 });
7767
7768 let dirty_multi_buffer = cx.new_view(|cx| {
7769 TestItem::new(cx)
7770 .with_dirty(true)
7771 .with_singleton(false)
7772 .with_label("Fake Project Search")
7773 .with_project_items(&[
7774 dirty_regular_buffer.read(cx).project_items[0].clone(),
7775 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7776 clear_regular_buffer.read(cx).project_items[0].clone(),
7777 ])
7778 });
7779 workspace.update(cx, |workspace, cx| {
7780 workspace.add_item(
7781 pane.clone(),
7782 Box::new(dirty_regular_buffer.clone()),
7783 None,
7784 false,
7785 false,
7786 cx,
7787 );
7788 workspace.add_item(
7789 pane.clone(),
7790 Box::new(dirty_regular_buffer_2.clone()),
7791 None,
7792 false,
7793 false,
7794 cx,
7795 );
7796 workspace.add_item(
7797 pane.clone(),
7798 Box::new(dirty_multi_buffer.clone()),
7799 None,
7800 false,
7801 false,
7802 cx,
7803 );
7804 });
7805
7806 pane.update(cx, |pane, cx| {
7807 pane.activate_item(2, true, true, cx);
7808 assert_eq!(
7809 pane.active_item().unwrap().item_id(),
7810 dirty_multi_buffer.item_id(),
7811 "Should select the multi buffer in the pane"
7812 );
7813 });
7814 let close_multi_buffer_task = pane
7815 .update(cx, |pane, cx| {
7816 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7817 })
7818 .expect("should have active multi buffer to close");
7819 cx.background_executor.run_until_parked();
7820 assert!(
7821 !cx.has_pending_prompt(),
7822 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
7823 );
7824 close_multi_buffer_task
7825 .await
7826 .expect("Closing multi buffer failed");
7827 pane.update(cx, |pane, cx| {
7828 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7829 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
7830 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7831 assert_eq!(
7832 pane.items()
7833 .map(|item| item.item_id())
7834 .sorted()
7835 .collect::<Vec<_>>(),
7836 vec![
7837 dirty_regular_buffer.item_id(),
7838 dirty_regular_buffer_2.item_id(),
7839 ],
7840 "Should have no multi buffer left in the pane"
7841 );
7842 assert!(dirty_regular_buffer.read(cx).is_dirty);
7843 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
7844 });
7845 }
7846
7847 mod register_project_item_tests {
7848 use ui::Context as _;
7849
7850 use super::*;
7851
7852 // View
7853 struct TestPngItemView {
7854 focus_handle: FocusHandle,
7855 }
7856 // Model
7857 struct TestPngItem {}
7858
7859 impl project::ProjectItem for TestPngItem {
7860 fn try_open(
7861 _project: &Model<Project>,
7862 path: &ProjectPath,
7863 cx: &mut AppContext,
7864 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7865 if path.path.extension().unwrap() == "png" {
7866 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7867 } else {
7868 None
7869 }
7870 }
7871
7872 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7873 None
7874 }
7875
7876 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7877 None
7878 }
7879
7880 fn is_dirty(&self) -> bool {
7881 false
7882 }
7883 }
7884
7885 impl Item for TestPngItemView {
7886 type Event = ();
7887 }
7888 impl EventEmitter<()> for TestPngItemView {}
7889 impl FocusableView for TestPngItemView {
7890 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7891 self.focus_handle.clone()
7892 }
7893 }
7894
7895 impl Render for TestPngItemView {
7896 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7897 Empty
7898 }
7899 }
7900
7901 impl ProjectItem for TestPngItemView {
7902 type Item = TestPngItem;
7903
7904 fn for_project_item(
7905 _project: Model<Project>,
7906 _item: Model<Self::Item>,
7907 cx: &mut ViewContext<Self>,
7908 ) -> Self
7909 where
7910 Self: Sized,
7911 {
7912 Self {
7913 focus_handle: cx.focus_handle(),
7914 }
7915 }
7916 }
7917
7918 // View
7919 struct TestIpynbItemView {
7920 focus_handle: FocusHandle,
7921 }
7922 // Model
7923 struct TestIpynbItem {}
7924
7925 impl project::ProjectItem for TestIpynbItem {
7926 fn try_open(
7927 _project: &Model<Project>,
7928 path: &ProjectPath,
7929 cx: &mut AppContext,
7930 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7931 if path.path.extension().unwrap() == "ipynb" {
7932 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7933 } else {
7934 None
7935 }
7936 }
7937
7938 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7939 None
7940 }
7941
7942 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7943 None
7944 }
7945
7946 fn is_dirty(&self) -> bool {
7947 false
7948 }
7949 }
7950
7951 impl Item for TestIpynbItemView {
7952 type Event = ();
7953 }
7954 impl EventEmitter<()> for TestIpynbItemView {}
7955 impl FocusableView for TestIpynbItemView {
7956 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7957 self.focus_handle.clone()
7958 }
7959 }
7960
7961 impl Render for TestIpynbItemView {
7962 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7963 Empty
7964 }
7965 }
7966
7967 impl ProjectItem for TestIpynbItemView {
7968 type Item = TestIpynbItem;
7969
7970 fn for_project_item(
7971 _project: Model<Project>,
7972 _item: Model<Self::Item>,
7973 cx: &mut ViewContext<Self>,
7974 ) -> Self
7975 where
7976 Self: Sized,
7977 {
7978 Self {
7979 focus_handle: cx.focus_handle(),
7980 }
7981 }
7982 }
7983
7984 struct TestAlternatePngItemView {
7985 focus_handle: FocusHandle,
7986 }
7987
7988 impl Item for TestAlternatePngItemView {
7989 type Event = ();
7990 }
7991
7992 impl EventEmitter<()> for TestAlternatePngItemView {}
7993 impl FocusableView for TestAlternatePngItemView {
7994 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7995 self.focus_handle.clone()
7996 }
7997 }
7998
7999 impl Render for TestAlternatePngItemView {
8000 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8001 Empty
8002 }
8003 }
8004
8005 impl ProjectItem for TestAlternatePngItemView {
8006 type Item = TestPngItem;
8007
8008 fn for_project_item(
8009 _project: Model<Project>,
8010 _item: Model<Self::Item>,
8011 cx: &mut ViewContext<Self>,
8012 ) -> Self
8013 where
8014 Self: Sized,
8015 {
8016 Self {
8017 focus_handle: cx.focus_handle(),
8018 }
8019 }
8020 }
8021
8022 #[gpui::test]
8023 async fn test_register_project_item(cx: &mut TestAppContext) {
8024 init_test(cx);
8025
8026 cx.update(|cx| {
8027 register_project_item::<TestPngItemView>(cx);
8028 register_project_item::<TestIpynbItemView>(cx);
8029 });
8030
8031 let fs = FakeFs::new(cx.executor());
8032 fs.insert_tree(
8033 "/root1",
8034 json!({
8035 "one.png": "BINARYDATAHERE",
8036 "two.ipynb": "{ totally a notebook }",
8037 "three.txt": "editing text, sure why not?"
8038 }),
8039 )
8040 .await;
8041
8042 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8043 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8044
8045 let worktree_id = project.update(cx, |project, cx| {
8046 project.worktrees(cx).next().unwrap().read(cx).id()
8047 });
8048
8049 let handle = workspace
8050 .update(cx, |workspace, cx| {
8051 let project_path = (worktree_id, "one.png");
8052 workspace.open_path(project_path, None, true, cx)
8053 })
8054 .await
8055 .unwrap();
8056
8057 // Now we can check if the handle we got back errored or not
8058 assert_eq!(
8059 handle.to_any().entity_type(),
8060 TypeId::of::<TestPngItemView>()
8061 );
8062
8063 let handle = workspace
8064 .update(cx, |workspace, cx| {
8065 let project_path = (worktree_id, "two.ipynb");
8066 workspace.open_path(project_path, None, true, cx)
8067 })
8068 .await
8069 .unwrap();
8070
8071 assert_eq!(
8072 handle.to_any().entity_type(),
8073 TypeId::of::<TestIpynbItemView>()
8074 );
8075
8076 let handle = workspace
8077 .update(cx, |workspace, cx| {
8078 let project_path = (worktree_id, "three.txt");
8079 workspace.open_path(project_path, None, true, cx)
8080 })
8081 .await;
8082 assert!(handle.is_err());
8083 }
8084
8085 #[gpui::test]
8086 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8087 init_test(cx);
8088
8089 cx.update(|cx| {
8090 register_project_item::<TestPngItemView>(cx);
8091 register_project_item::<TestAlternatePngItemView>(cx);
8092 });
8093
8094 let fs = FakeFs::new(cx.executor());
8095 fs.insert_tree(
8096 "/root1",
8097 json!({
8098 "one.png": "BINARYDATAHERE",
8099 "two.ipynb": "{ totally a notebook }",
8100 "three.txt": "editing text, sure why not?"
8101 }),
8102 )
8103 .await;
8104
8105 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8106 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8107
8108 let worktree_id = project.update(cx, |project, cx| {
8109 project.worktrees(cx).next().unwrap().read(cx).id()
8110 });
8111
8112 let handle = workspace
8113 .update(cx, |workspace, cx| {
8114 let project_path = (worktree_id, "one.png");
8115 workspace.open_path(project_path, None, true, cx)
8116 })
8117 .await
8118 .unwrap();
8119
8120 // This _must_ be the second item registered
8121 assert_eq!(
8122 handle.to_any().entity_type(),
8123 TypeId::of::<TestAlternatePngItemView>()
8124 );
8125
8126 let handle = workspace
8127 .update(cx, |workspace, cx| {
8128 let project_path = (worktree_id, "three.txt");
8129 workspace.open_path(project_path, None, true, cx)
8130 })
8131 .await;
8132 assert!(handle.is_err());
8133 }
8134 }
8135
8136 pub fn init_test(cx: &mut TestAppContext) {
8137 cx.update(|cx| {
8138 let settings_store = SettingsStore::test(cx);
8139 cx.set_global(settings_store);
8140 theme::init(theme::LoadThemes::JustBase, cx);
8141 language::init(cx);
8142 crate::init_settings(cx);
8143 Project::init_settings(cx);
8144 });
8145 }
8146
8147 fn dirty_project_item(id: u64, path: &str, cx: &mut AppContext) -> Model<TestProjectItem> {
8148 let item = TestProjectItem::new(id, path, cx);
8149 item.update(cx, |item, _| {
8150 item.is_dirty = true;
8151 });
8152 item
8153 }
8154}