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