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