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