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.inner_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, |_, cx| {
1710 if cx.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 cx.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 dock.active_panel().is_none() && dock.panels_len() > 0 {
2301 dock.activate_panel(0, cx);
2302 }
2303
2304 if let Some(active_panel) = dock.active_panel() {
2305 if was_visible {
2306 if active_panel.focus_handle(cx).contains_focused(cx) {
2307 focus_center = true;
2308 }
2309 } else {
2310 let focus_handle = &active_panel.focus_handle(cx);
2311 cx.focus(focus_handle);
2312 reveal_dock = true;
2313 }
2314 }
2315 });
2316
2317 if reveal_dock {
2318 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
2319 }
2320
2321 if focus_center {
2322 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2323 }
2324
2325 cx.notify();
2326 self.serialize_workspace(cx);
2327 }
2328
2329 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
2330 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
2331
2332 for dock in docks {
2333 dock.update(cx, |dock, cx| {
2334 dock.set_open(false, cx);
2335 });
2336 }
2337
2338 cx.focus_self();
2339 cx.notify();
2340 self.serialize_workspace(cx);
2341 }
2342
2343 /// Transfer focus to the panel of the given type.
2344 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
2345 let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
2346 panel.to_any().downcast().ok()
2347 }
2348
2349 /// Focus the panel of the given type if it isn't already focused. If it is
2350 /// already focused, then transfer focus back to the workspace center.
2351 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2352 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
2353 !panel.focus_handle(cx).contains_focused(cx)
2354 });
2355 }
2356
2357 pub fn activate_panel_for_proto_id(
2358 &mut self,
2359 panel_id: PanelId,
2360 cx: &mut ViewContext<Self>,
2361 ) -> Option<Arc<dyn PanelHandle>> {
2362 let mut panel = None;
2363 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2364 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2365 panel = dock.update(cx, |dock, cx| {
2366 dock.activate_panel(panel_index, cx);
2367 dock.set_open(true, cx);
2368 dock.active_panel().cloned()
2369 });
2370 break;
2371 }
2372 }
2373
2374 if panel.is_some() {
2375 cx.notify();
2376 self.serialize_workspace(cx);
2377 }
2378
2379 panel
2380 }
2381
2382 /// Focus or unfocus the given panel type, depending on the given callback.
2383 fn focus_or_unfocus_panel<T: Panel>(
2384 &mut self,
2385 cx: &mut ViewContext<Self>,
2386 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
2387 ) -> Option<Arc<dyn PanelHandle>> {
2388 let mut result_panel = None;
2389 let mut serialize = false;
2390 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2391 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2392 let mut focus_center = false;
2393 let panel = dock.update(cx, |dock, cx| {
2394 dock.activate_panel(panel_index, cx);
2395
2396 let panel = dock.active_panel().cloned();
2397 if let Some(panel) = panel.as_ref() {
2398 if should_focus(&**panel, cx) {
2399 dock.set_open(true, cx);
2400 panel.focus_handle(cx).focus(cx);
2401 } else {
2402 focus_center = true;
2403 }
2404 }
2405 panel
2406 });
2407
2408 if focus_center {
2409 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2410 }
2411
2412 result_panel = panel;
2413 serialize = true;
2414 break;
2415 }
2416 }
2417
2418 if serialize {
2419 self.serialize_workspace(cx);
2420 }
2421
2422 cx.notify();
2423 result_panel
2424 }
2425
2426 /// Open the panel of the given type
2427 pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2428 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2429 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2430 dock.update(cx, |dock, cx| {
2431 dock.activate_panel(panel_index, cx);
2432 dock.set_open(true, cx);
2433 });
2434 }
2435 }
2436 }
2437
2438 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
2439 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2440 .iter()
2441 .find_map(|dock| dock.read(cx).panel::<T>())
2442 }
2443
2444 fn dismiss_zoomed_items_to_reveal(
2445 &mut self,
2446 dock_to_reveal: Option<DockPosition>,
2447 cx: &mut ViewContext<Self>,
2448 ) {
2449 // If a center pane is zoomed, unzoom it.
2450 for pane in &self.panes {
2451 if pane != &self.active_pane || dock_to_reveal.is_some() {
2452 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2453 }
2454 }
2455
2456 // If another dock is zoomed, hide it.
2457 let mut focus_center = false;
2458 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2459 dock.update(cx, |dock, cx| {
2460 if Some(dock.position()) != dock_to_reveal {
2461 if let Some(panel) = dock.active_panel() {
2462 if panel.is_zoomed(cx) {
2463 focus_center |= panel.focus_handle(cx).contains_focused(cx);
2464 dock.set_open(false, cx);
2465 }
2466 }
2467 }
2468 });
2469 }
2470
2471 if focus_center {
2472 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2473 }
2474
2475 if self.zoomed_position != dock_to_reveal {
2476 self.zoomed = None;
2477 self.zoomed_position = None;
2478 cx.emit(Event::ZoomChanged);
2479 }
2480
2481 cx.notify();
2482 }
2483
2484 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2485 let pane = cx.new_view(|cx| {
2486 let mut pane = Pane::new(
2487 self.weak_handle(),
2488 self.project.clone(),
2489 self.pane_history_timestamp.clone(),
2490 None,
2491 NewFile.boxed_clone(),
2492 cx,
2493 );
2494 pane.set_can_split(Some(Arc::new(|_, _, _| true)));
2495 pane
2496 });
2497 cx.subscribe(&pane, Self::handle_pane_event).detach();
2498 self.panes.push(pane.clone());
2499 cx.focus_view(&pane);
2500 cx.emit(Event::PaneAdded(pane.clone()));
2501 pane
2502 }
2503
2504 pub fn add_item_to_center(
2505 &mut self,
2506 item: Box<dyn ItemHandle>,
2507 cx: &mut ViewContext<Self>,
2508 ) -> bool {
2509 if let Some(center_pane) = self.last_active_center_pane.clone() {
2510 if let Some(center_pane) = center_pane.upgrade() {
2511 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2512 true
2513 } else {
2514 false
2515 }
2516 } else {
2517 false
2518 }
2519 }
2520
2521 pub fn add_item_to_active_pane(
2522 &mut self,
2523 item: Box<dyn ItemHandle>,
2524 destination_index: Option<usize>,
2525 focus_item: bool,
2526 cx: &mut WindowContext,
2527 ) {
2528 self.add_item(
2529 self.active_pane.clone(),
2530 item,
2531 destination_index,
2532 false,
2533 focus_item,
2534 cx,
2535 )
2536 }
2537
2538 pub fn add_item(
2539 &mut self,
2540 pane: View<Pane>,
2541 item: Box<dyn ItemHandle>,
2542 destination_index: Option<usize>,
2543 activate_pane: bool,
2544 focus_item: bool,
2545 cx: &mut WindowContext,
2546 ) {
2547 if let Some(text) = item.telemetry_event_text(cx) {
2548 self.client()
2549 .telemetry()
2550 .report_app_event(format!("{}: open", text));
2551 }
2552
2553 pane.update(cx, |pane, cx| {
2554 pane.add_item(item, activate_pane, focus_item, destination_index, cx)
2555 });
2556 }
2557
2558 pub fn split_item(
2559 &mut self,
2560 split_direction: SplitDirection,
2561 item: Box<dyn ItemHandle>,
2562 cx: &mut ViewContext<Self>,
2563 ) {
2564 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2565 self.add_item(new_pane, item, None, true, true, cx);
2566 }
2567
2568 pub fn open_abs_path(
2569 &mut self,
2570 abs_path: PathBuf,
2571 visible: bool,
2572 cx: &mut ViewContext<Self>,
2573 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2574 cx.spawn(|workspace, mut cx| async move {
2575 let open_paths_task_result = workspace
2576 .update(&mut cx, |workspace, cx| {
2577 workspace.open_paths(
2578 vec![abs_path.clone()],
2579 if visible {
2580 OpenVisible::All
2581 } else {
2582 OpenVisible::None
2583 },
2584 None,
2585 cx,
2586 )
2587 })
2588 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2589 .await;
2590 anyhow::ensure!(
2591 open_paths_task_result.len() == 1,
2592 "open abs path {abs_path:?} task returned incorrect number of results"
2593 );
2594 match open_paths_task_result
2595 .into_iter()
2596 .next()
2597 .expect("ensured single task result")
2598 {
2599 Some(open_result) => {
2600 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2601 }
2602 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2603 }
2604 })
2605 }
2606
2607 pub fn split_abs_path(
2608 &mut self,
2609 abs_path: PathBuf,
2610 visible: bool,
2611 cx: &mut ViewContext<Self>,
2612 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2613 let project_path_task =
2614 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2615 cx.spawn(|this, mut cx| async move {
2616 let (_, path) = project_path_task.await?;
2617 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2618 .await
2619 })
2620 }
2621
2622 pub fn open_path(
2623 &mut self,
2624 path: impl Into<ProjectPath>,
2625 pane: Option<WeakView<Pane>>,
2626 focus_item: bool,
2627 cx: &mut WindowContext,
2628 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2629 self.open_path_preview(path, pane, focus_item, false, cx)
2630 }
2631
2632 pub fn open_path_preview(
2633 &mut self,
2634 path: impl Into<ProjectPath>,
2635 pane: Option<WeakView<Pane>>,
2636 focus_item: bool,
2637 allow_preview: bool,
2638 cx: &mut WindowContext,
2639 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2640 let pane = pane.unwrap_or_else(|| {
2641 self.last_active_center_pane.clone().unwrap_or_else(|| {
2642 self.panes
2643 .first()
2644 .expect("There must be an active pane")
2645 .downgrade()
2646 })
2647 });
2648
2649 let task = self.load_path(path.into(), cx);
2650 cx.spawn(move |mut cx| async move {
2651 let (project_entry_id, build_item) = task.await?;
2652 pane.update(&mut cx, |pane, cx| {
2653 pane.open_item(
2654 project_entry_id,
2655 focus_item,
2656 allow_preview,
2657 None,
2658 cx,
2659 build_item,
2660 )
2661 })
2662 })
2663 }
2664
2665 pub fn split_path(
2666 &mut self,
2667 path: impl Into<ProjectPath>,
2668 cx: &mut ViewContext<Self>,
2669 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2670 self.split_path_preview(path, false, None, cx)
2671 }
2672
2673 pub fn split_path_preview(
2674 &mut self,
2675 path: impl Into<ProjectPath>,
2676 allow_preview: bool,
2677 split_direction: Option<SplitDirection>,
2678 cx: &mut ViewContext<Self>,
2679 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2680 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2681 self.panes
2682 .first()
2683 .expect("There must be an active pane")
2684 .downgrade()
2685 });
2686
2687 if let Member::Pane(center_pane) = &self.center.root {
2688 if center_pane.read(cx).items_len() == 0 {
2689 return self.open_path(path, Some(pane), true, cx);
2690 }
2691 }
2692
2693 let task = self.load_path(path.into(), cx);
2694 cx.spawn(|this, mut cx| async move {
2695 let (project_entry_id, build_item) = task.await?;
2696 this.update(&mut cx, move |this, cx| -> Option<_> {
2697 let pane = pane.upgrade()?;
2698 let new_pane =
2699 this.split_pane(pane, split_direction.unwrap_or(SplitDirection::Right), cx);
2700 new_pane.update(cx, |new_pane, cx| {
2701 Some(new_pane.open_item(
2702 project_entry_id,
2703 true,
2704 allow_preview,
2705 None,
2706 cx,
2707 build_item,
2708 ))
2709 })
2710 })
2711 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2712 })
2713 }
2714
2715 fn load_path(
2716 &mut self,
2717 path: ProjectPath,
2718 cx: &mut WindowContext,
2719 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2720 let project = self.project().clone();
2721 let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2722 let Some(open_project_item) = project_item_builders
2723 .iter()
2724 .rev()
2725 .find_map(|open_project_item| open_project_item(&project, &path, cx))
2726 else {
2727 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2728 };
2729 open_project_item
2730 }
2731
2732 pub fn find_project_item<T>(
2733 &self,
2734 pane: &View<Pane>,
2735 project_item: &Model<T::Item>,
2736 cx: &AppContext,
2737 ) -> Option<View<T>>
2738 where
2739 T: ProjectItem,
2740 {
2741 use project::ProjectItem as _;
2742 let project_item = project_item.read(cx);
2743 let entry_id = project_item.entry_id(cx);
2744 let project_path = project_item.project_path(cx);
2745
2746 let mut item = None;
2747 if let Some(entry_id) = entry_id {
2748 item = pane.read(cx).item_for_entry(entry_id, cx);
2749 }
2750 if item.is_none() {
2751 if let Some(project_path) = project_path {
2752 item = pane.read(cx).item_for_path(project_path, cx);
2753 }
2754 }
2755
2756 item.and_then(|item| item.downcast::<T>())
2757 }
2758
2759 pub fn is_project_item_open<T>(
2760 &self,
2761 pane: &View<Pane>,
2762 project_item: &Model<T::Item>,
2763 cx: &AppContext,
2764 ) -> bool
2765 where
2766 T: ProjectItem,
2767 {
2768 self.find_project_item::<T>(pane, project_item, cx)
2769 .is_some()
2770 }
2771
2772 pub fn open_project_item<T>(
2773 &mut self,
2774 pane: View<Pane>,
2775 project_item: Model<T::Item>,
2776 activate_pane: bool,
2777 focus_item: bool,
2778 cx: &mut ViewContext<Self>,
2779 ) -> View<T>
2780 where
2781 T: ProjectItem,
2782 {
2783 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
2784 self.activate_item(&item, activate_pane, focus_item, cx);
2785 return item;
2786 }
2787
2788 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2789 let item_id = item.item_id();
2790 let mut destination_index = None;
2791 pane.update(cx, |pane, cx| {
2792 if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2793 if let Some(preview_item_id) = pane.preview_item_id() {
2794 if preview_item_id != item_id {
2795 destination_index = pane.close_current_preview_item(cx);
2796 }
2797 }
2798 }
2799 pane.set_preview_item_id(Some(item.item_id()), cx)
2800 });
2801
2802 self.add_item(
2803 pane,
2804 Box::new(item.clone()),
2805 destination_index,
2806 activate_pane,
2807 focus_item,
2808 cx,
2809 );
2810 item
2811 }
2812
2813 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2814 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2815 self.active_pane.update(cx, |pane, cx| {
2816 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2817 });
2818 }
2819 }
2820
2821 pub fn activate_item(
2822 &mut self,
2823 item: &dyn ItemHandle,
2824 activate_pane: bool,
2825 focus_item: bool,
2826 cx: &mut WindowContext,
2827 ) -> bool {
2828 let result = self.panes.iter().find_map(|pane| {
2829 pane.read(cx)
2830 .index_for_item(item)
2831 .map(|ix| (pane.clone(), ix))
2832 });
2833 if let Some((pane, ix)) = result {
2834 pane.update(cx, |pane, cx| {
2835 pane.activate_item(ix, activate_pane, focus_item, cx)
2836 });
2837 true
2838 } else {
2839 false
2840 }
2841 }
2842
2843 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2844 let panes = self.center.panes();
2845 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2846 cx.focus_view(&pane);
2847 } else {
2848 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2849 }
2850 }
2851
2852 fn move_item_to_pane_at_index(&mut self, action: &MoveItemToPane, cx: &mut ViewContext<Self>) {
2853 let Some(&target_pane) = self.center.panes().get(action.destination) else {
2854 return;
2855 };
2856 move_active_item(&self.active_pane, target_pane, action.focus, true, cx);
2857 }
2858
2859 pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2860 let panes = self.center.panes();
2861 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2862 let next_ix = (ix + 1) % panes.len();
2863 let next_pane = panes[next_ix].clone();
2864 cx.focus_view(&next_pane);
2865 }
2866 }
2867
2868 pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2869 let panes = self.center.panes();
2870 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2871 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2872 let prev_pane = panes[prev_ix].clone();
2873 cx.focus_view(&prev_pane);
2874 }
2875 }
2876
2877 pub fn activate_pane_in_direction(
2878 &mut self,
2879 direction: SplitDirection,
2880 cx: &mut WindowContext,
2881 ) {
2882 use ActivateInDirectionTarget as Target;
2883 enum Origin {
2884 LeftDock,
2885 RightDock,
2886 BottomDock,
2887 Center,
2888 }
2889
2890 let origin: Origin = [
2891 (&self.left_dock, Origin::LeftDock),
2892 (&self.right_dock, Origin::RightDock),
2893 (&self.bottom_dock, Origin::BottomDock),
2894 ]
2895 .into_iter()
2896 .find_map(|(dock, origin)| {
2897 if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2898 Some(origin)
2899 } else {
2900 None
2901 }
2902 })
2903 .unwrap_or(Origin::Center);
2904
2905 let get_last_active_pane = || {
2906 let pane = self
2907 .last_active_center_pane
2908 .clone()
2909 .unwrap_or_else(|| {
2910 self.panes
2911 .first()
2912 .expect("There must be an active pane")
2913 .downgrade()
2914 })
2915 .upgrade()?;
2916 (pane.read(cx).items_len() != 0).then_some(pane)
2917 };
2918
2919 let try_dock =
2920 |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2921
2922 let target = match (origin, direction) {
2923 // We're in the center, so we first try to go to a different pane,
2924 // otherwise try to go to a dock.
2925 (Origin::Center, direction) => {
2926 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2927 Some(Target::Pane(pane))
2928 } else {
2929 match direction {
2930 SplitDirection::Up => None,
2931 SplitDirection::Down => try_dock(&self.bottom_dock),
2932 SplitDirection::Left => try_dock(&self.left_dock),
2933 SplitDirection::Right => try_dock(&self.right_dock),
2934 }
2935 }
2936 }
2937
2938 (Origin::LeftDock, SplitDirection::Right) => {
2939 if let Some(last_active_pane) = get_last_active_pane() {
2940 Some(Target::Pane(last_active_pane))
2941 } else {
2942 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2943 }
2944 }
2945
2946 (Origin::LeftDock, SplitDirection::Down)
2947 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2948
2949 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2950 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2951 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2952
2953 (Origin::RightDock, SplitDirection::Left) => {
2954 if let Some(last_active_pane) = get_last_active_pane() {
2955 Some(Target::Pane(last_active_pane))
2956 } else {
2957 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2958 }
2959 }
2960
2961 _ => None,
2962 };
2963
2964 match target {
2965 Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2966 Some(ActivateInDirectionTarget::Dock(dock)) => {
2967 dock.update(cx, |dock, cx| {
2968 if let Some(panel) = dock.active_panel() {
2969 panel.focus_handle(cx).focus(cx);
2970 } else {
2971 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
2972 }
2973 });
2974 }
2975 None => {}
2976 }
2977 }
2978
2979 pub fn move_item_to_pane_in_direction(
2980 &mut self,
2981 action: &MoveItemToPaneInDirection,
2982 cx: &mut WindowContext,
2983 ) {
2984 if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
2985 move_active_item(&self.active_pane, &destination, action.focus, true, cx);
2986 }
2987 }
2988
2989 pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
2990 self.center.bounding_box_for_pane(pane)
2991 }
2992
2993 pub fn find_pane_in_direction(
2994 &mut self,
2995 direction: SplitDirection,
2996 cx: &WindowContext,
2997 ) -> Option<View<Pane>> {
2998 self.center
2999 .find_pane_in_direction(&self.active_pane, direction, cx)
3000 .cloned()
3001 }
3002
3003 pub fn swap_pane_in_direction(
3004 &mut self,
3005 direction: SplitDirection,
3006 cx: &mut ViewContext<Self>,
3007 ) {
3008 if let Some(to) = self.find_pane_in_direction(direction, cx) {
3009 self.center.swap(&self.active_pane, &to);
3010 cx.notify();
3011 }
3012 }
3013
3014 pub fn resize_pane(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut ViewContext<Self>) {
3015 self.center
3016 .resize(&self.active_pane, axis, amount, &self.bounds);
3017 cx.notify();
3018 }
3019
3020 pub fn reset_pane_sizes(&mut self, cx: &mut ViewContext<Self>) {
3021 self.center.reset_pane_sizes();
3022 cx.notify();
3023 }
3024
3025 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3026 // This is explicitly hoisted out of the following check for pane identity as
3027 // terminal panel panes are not registered as a center panes.
3028 self.status_bar.update(cx, |status_bar, cx| {
3029 status_bar.set_active_pane(&pane, cx);
3030 });
3031 if self.active_pane != pane {
3032 self.set_active_pane(&pane, cx);
3033 }
3034
3035 if self.last_active_center_pane.is_none() {
3036 self.last_active_center_pane = Some(pane.downgrade());
3037 }
3038
3039 self.dismiss_zoomed_items_to_reveal(None, cx);
3040 if pane.read(cx).is_zoomed() {
3041 self.zoomed = Some(pane.downgrade().into());
3042 } else {
3043 self.zoomed = None;
3044 }
3045 self.zoomed_position = None;
3046 cx.emit(Event::ZoomChanged);
3047 self.update_active_view_for_followers(cx);
3048 pane.model.update(cx, |pane, _| {
3049 pane.track_alternate_file_items();
3050 });
3051
3052 cx.notify();
3053 }
3054
3055 fn set_active_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) {
3056 self.active_pane = pane.clone();
3057 self.active_item_path_changed(cx);
3058 self.last_active_center_pane = Some(pane.downgrade());
3059 }
3060
3061 fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
3062 self.update_active_view_for_followers(cx);
3063 }
3064
3065 fn handle_pane_event(
3066 &mut self,
3067 pane: View<Pane>,
3068 event: &pane::Event,
3069 cx: &mut ViewContext<Self>,
3070 ) {
3071 match event {
3072 pane::Event::AddItem { item } => {
3073 item.added_to_pane(self, pane, cx);
3074 cx.emit(Event::ItemAdded {
3075 item: item.boxed_clone(),
3076 });
3077 }
3078 pane::Event::Split(direction) => {
3079 self.split_and_clone(pane, *direction, cx);
3080 }
3081 pane::Event::JoinIntoNext => self.join_pane_into_next(pane, cx),
3082 pane::Event::JoinAll => self.join_all_panes(cx),
3083 pane::Event::Remove { focus_on_pane } => {
3084 self.remove_pane(pane, focus_on_pane.clone(), cx)
3085 }
3086 pane::Event::ActivateItem { local } => {
3087 cx.on_next_frame(|_, cx| {
3088 cx.invalidate_character_coordinates();
3089 });
3090
3091 pane.model.update(cx, |pane, _| {
3092 pane.track_alternate_file_items();
3093 });
3094 if *local {
3095 self.unfollow_in_pane(&pane, cx);
3096 }
3097 if &pane == self.active_pane() {
3098 self.active_item_path_changed(cx);
3099 self.update_active_view_for_followers(cx);
3100 }
3101 }
3102 pane::Event::UserSavedItem { item, save_intent } => cx.emit(Event::UserSavedItem {
3103 pane: pane.downgrade(),
3104 item: item.boxed_clone(),
3105 save_intent: *save_intent,
3106 }),
3107 pane::Event::ChangeItemTitle => {
3108 if pane == self.active_pane {
3109 self.active_item_path_changed(cx);
3110 }
3111 self.update_window_edited(cx);
3112 }
3113 pane::Event::RemoveItem { .. } => {}
3114 pane::Event::RemovedItem { item_id } => {
3115 cx.emit(Event::ActiveItemChanged);
3116 self.update_window_edited(cx);
3117 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3118 if entry.get().entity_id() == pane.entity_id() {
3119 entry.remove();
3120 }
3121 }
3122 }
3123 pane::Event::Focus => {
3124 cx.on_next_frame(|_, cx| {
3125 cx.invalidate_character_coordinates();
3126 });
3127 self.handle_pane_focused(pane.clone(), cx);
3128 }
3129 pane::Event::ZoomIn => {
3130 if pane == self.active_pane {
3131 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3132 if pane.read(cx).has_focus(cx) {
3133 self.zoomed = Some(pane.downgrade().into());
3134 self.zoomed_position = None;
3135 cx.emit(Event::ZoomChanged);
3136 }
3137 cx.notify();
3138 }
3139 }
3140 pane::Event::ZoomOut => {
3141 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3142 if self.zoomed_position.is_none() {
3143 self.zoomed = None;
3144 cx.emit(Event::ZoomChanged);
3145 }
3146 cx.notify();
3147 }
3148 }
3149
3150 self.serialize_workspace(cx);
3151 }
3152
3153 pub fn unfollow_in_pane(
3154 &mut self,
3155 pane: &View<Pane>,
3156 cx: &mut ViewContext<Workspace>,
3157 ) -> Option<PeerId> {
3158 let leader_id = self.leader_for_pane(pane)?;
3159 self.unfollow(leader_id, cx);
3160 Some(leader_id)
3161 }
3162
3163 pub fn split_pane(
3164 &mut self,
3165 pane_to_split: View<Pane>,
3166 split_direction: SplitDirection,
3167 cx: &mut ViewContext<Self>,
3168 ) -> View<Pane> {
3169 let new_pane = self.add_pane(cx);
3170 self.center
3171 .split(&pane_to_split, &new_pane, split_direction)
3172 .unwrap();
3173 cx.notify();
3174 new_pane
3175 }
3176
3177 pub fn split_and_clone(
3178 &mut self,
3179 pane: View<Pane>,
3180 direction: SplitDirection,
3181 cx: &mut ViewContext<Self>,
3182 ) -> Option<View<Pane>> {
3183 let item = pane.read(cx).active_item()?;
3184 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3185 let new_pane = self.add_pane(cx);
3186 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3187 self.center.split(&pane, &new_pane, direction).unwrap();
3188 Some(new_pane)
3189 } else {
3190 None
3191 };
3192 cx.notify();
3193 maybe_pane_handle
3194 }
3195
3196 pub fn split_pane_with_item(
3197 &mut self,
3198 pane_to_split: WeakView<Pane>,
3199 split_direction: SplitDirection,
3200 from: WeakView<Pane>,
3201 item_id_to_move: EntityId,
3202 cx: &mut ViewContext<Self>,
3203 ) {
3204 let Some(pane_to_split) = pane_to_split.upgrade() else {
3205 return;
3206 };
3207 let Some(from) = from.upgrade() else {
3208 return;
3209 };
3210
3211 let new_pane = self.add_pane(cx);
3212 move_item(&from, &new_pane, item_id_to_move, 0, cx);
3213 self.center
3214 .split(&pane_to_split, &new_pane, split_direction)
3215 .unwrap();
3216 cx.notify();
3217 }
3218
3219 pub fn split_pane_with_project_entry(
3220 &mut self,
3221 pane_to_split: WeakView<Pane>,
3222 split_direction: SplitDirection,
3223 project_entry: ProjectEntryId,
3224 cx: &mut ViewContext<Self>,
3225 ) -> Option<Task<Result<()>>> {
3226 let pane_to_split = pane_to_split.upgrade()?;
3227 let new_pane = self.add_pane(cx);
3228 self.center
3229 .split(&pane_to_split, &new_pane, split_direction)
3230 .unwrap();
3231
3232 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3233 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3234 Some(cx.foreground_executor().spawn(async move {
3235 task.await?;
3236 Ok(())
3237 }))
3238 }
3239
3240 pub fn join_all_panes(&mut self, cx: &mut ViewContext<Self>) {
3241 let active_item = self.active_pane.read(cx).active_item();
3242 for pane in &self.panes {
3243 join_pane_into_active(&self.active_pane, pane, cx);
3244 }
3245 if let Some(active_item) = active_item {
3246 self.activate_item(active_item.as_ref(), true, true, cx);
3247 }
3248 cx.notify();
3249 }
3250
3251 pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3252 let next_pane = self
3253 .find_pane_in_direction(SplitDirection::Right, cx)
3254 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3255 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3256 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3257 let Some(next_pane) = next_pane else {
3258 return;
3259 };
3260 move_all_items(&pane, &next_pane, cx);
3261 cx.notify();
3262 }
3263
3264 fn remove_pane(
3265 &mut self,
3266 pane: View<Pane>,
3267 focus_on: Option<View<Pane>>,
3268 cx: &mut ViewContext<Self>,
3269 ) {
3270 if self.center.remove(&pane).unwrap() {
3271 self.force_remove_pane(&pane, &focus_on, cx);
3272 self.unfollow_in_pane(&pane, cx);
3273 self.last_leaders_by_pane.remove(&pane.downgrade());
3274 for removed_item in pane.read(cx).items() {
3275 self.panes_by_item.remove(&removed_item.item_id());
3276 }
3277
3278 cx.notify();
3279 } else {
3280 self.active_item_path_changed(cx);
3281 }
3282 cx.emit(Event::PaneRemoved);
3283 }
3284
3285 pub fn panes(&self) -> &[View<Pane>] {
3286 &self.panes
3287 }
3288
3289 pub fn active_pane(&self) -> &View<Pane> {
3290 &self.active_pane
3291 }
3292
3293 pub fn focused_pane(&self, cx: &WindowContext) -> View<Pane> {
3294 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3295 if dock.focus_handle(cx).contains_focused(cx) {
3296 if let Some(pane) = dock
3297 .read(cx)
3298 .active_panel()
3299 .and_then(|panel| panel.pane(cx))
3300 {
3301 return pane;
3302 }
3303 }
3304 }
3305 self.active_pane().clone()
3306 }
3307
3308 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3309 self.find_pane_in_direction(SplitDirection::Right, cx)
3310 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3311 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3312 .clone()
3313 }
3314
3315 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3316 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3317 weak_pane.upgrade()
3318 }
3319
3320 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3321 self.follower_states.retain(|leader_id, state| {
3322 if *leader_id == peer_id {
3323 for item in state.items_by_leader_view_id.values() {
3324 item.view.set_leader_peer_id(None, cx);
3325 }
3326 false
3327 } else {
3328 true
3329 }
3330 });
3331 cx.notify();
3332 }
3333
3334 pub fn start_following(
3335 &mut self,
3336 leader_id: PeerId,
3337 cx: &mut ViewContext<Self>,
3338 ) -> Option<Task<Result<()>>> {
3339 let pane = self.active_pane().clone();
3340
3341 self.last_leaders_by_pane
3342 .insert(pane.downgrade(), leader_id);
3343 self.unfollow(leader_id, cx);
3344 self.unfollow_in_pane(&pane, cx);
3345 self.follower_states.insert(
3346 leader_id,
3347 FollowerState {
3348 center_pane: pane.clone(),
3349 dock_pane: None,
3350 active_view_id: None,
3351 items_by_leader_view_id: Default::default(),
3352 },
3353 );
3354 cx.notify();
3355
3356 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3357 let project_id = self.project.read(cx).remote_id();
3358 let request = self.app_state.client.request(proto::Follow {
3359 room_id,
3360 project_id,
3361 leader_id: Some(leader_id),
3362 });
3363
3364 Some(cx.spawn(|this, mut cx| async move {
3365 let response = request.await?;
3366 this.update(&mut cx, |this, _| {
3367 let state = this
3368 .follower_states
3369 .get_mut(&leader_id)
3370 .ok_or_else(|| anyhow!("following interrupted"))?;
3371 state.active_view_id = response
3372 .active_view
3373 .as_ref()
3374 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3375 Ok::<_, anyhow::Error>(())
3376 })??;
3377 if let Some(view) = response.active_view {
3378 Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3379 }
3380 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3381 Ok(())
3382 }))
3383 }
3384
3385 pub fn follow_next_collaborator(
3386 &mut self,
3387 _: &FollowNextCollaborator,
3388 cx: &mut ViewContext<Self>,
3389 ) {
3390 let collaborators = self.project.read(cx).collaborators();
3391 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3392 let mut collaborators = collaborators.keys().copied();
3393 for peer_id in collaborators.by_ref() {
3394 if peer_id == leader_id {
3395 break;
3396 }
3397 }
3398 collaborators.next()
3399 } else if let Some(last_leader_id) =
3400 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3401 {
3402 if collaborators.contains_key(last_leader_id) {
3403 Some(*last_leader_id)
3404 } else {
3405 None
3406 }
3407 } else {
3408 None
3409 };
3410
3411 let pane = self.active_pane.clone();
3412 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3413 else {
3414 return;
3415 };
3416 if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3417 return;
3418 }
3419 if let Some(task) = self.start_following(leader_id, cx) {
3420 task.detach_and_log_err(cx)
3421 }
3422 }
3423
3424 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3425 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3426 return;
3427 };
3428 let room = room.read(cx);
3429 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3430 return;
3431 };
3432
3433 let project = self.project.read(cx);
3434
3435 let other_project_id = match remote_participant.location {
3436 call::ParticipantLocation::External => None,
3437 call::ParticipantLocation::UnsharedProject => None,
3438 call::ParticipantLocation::SharedProject { project_id } => {
3439 if Some(project_id) == project.remote_id() {
3440 None
3441 } else {
3442 Some(project_id)
3443 }
3444 }
3445 };
3446
3447 // if they are active in another project, follow there.
3448 if let Some(project_id) = other_project_id {
3449 let app_state = self.app_state.clone();
3450 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3451 .detach_and_log_err(cx);
3452 }
3453
3454 // if you're already following, find the right pane and focus it.
3455 if let Some(follower_state) = self.follower_states.get(&leader_id) {
3456 cx.focus_view(follower_state.pane());
3457 return;
3458 }
3459
3460 // Otherwise, follow.
3461 if let Some(task) = self.start_following(leader_id, cx) {
3462 task.detach_and_log_err(cx)
3463 }
3464 }
3465
3466 pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3467 cx.notify();
3468 let state = self.follower_states.remove(&leader_id)?;
3469 for (_, item) in state.items_by_leader_view_id {
3470 item.view.set_leader_peer_id(None, cx);
3471 }
3472
3473 let project_id = self.project.read(cx).remote_id();
3474 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3475 self.app_state
3476 .client
3477 .send(proto::Unfollow {
3478 room_id,
3479 project_id,
3480 leader_id: Some(leader_id),
3481 })
3482 .log_err();
3483
3484 Some(())
3485 }
3486
3487 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3488 self.follower_states.contains_key(&peer_id)
3489 }
3490
3491 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3492 cx.emit(Event::ActiveItemChanged);
3493 let active_entry = self.active_project_path(cx);
3494 self.project
3495 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3496
3497 self.update_window_title(cx);
3498 }
3499
3500 fn update_window_title(&mut self, cx: &mut WindowContext) {
3501 let project = self.project().read(cx);
3502 let mut title = String::new();
3503
3504 for (i, name) in project.worktree_root_names(cx).enumerate() {
3505 if i > 0 {
3506 title.push_str(", ");
3507 }
3508 title.push_str(name);
3509 }
3510
3511 if title.is_empty() {
3512 title = "empty project".to_string();
3513 }
3514
3515 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3516 let filename = path
3517 .path
3518 .file_name()
3519 .map(|s| s.to_string_lossy())
3520 .or_else(|| {
3521 Some(Cow::Borrowed(
3522 project
3523 .worktree_for_id(path.worktree_id, cx)?
3524 .read(cx)
3525 .root_name(),
3526 ))
3527 });
3528
3529 if let Some(filename) = filename {
3530 title.push_str(" — ");
3531 title.push_str(filename.as_ref());
3532 }
3533 }
3534
3535 if project.is_via_collab() {
3536 title.push_str(" ↙");
3537 } else if project.is_shared() {
3538 title.push_str(" ↗");
3539 }
3540
3541 cx.set_window_title(&title);
3542 }
3543
3544 fn update_window_edited(&mut self, cx: &mut WindowContext) {
3545 let is_edited = !self.project.read(cx).is_disconnected(cx)
3546 && self
3547 .items(cx)
3548 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3549 if is_edited != self.window_edited {
3550 self.window_edited = is_edited;
3551 cx.set_window_edited(self.window_edited)
3552 }
3553 }
3554
3555 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3556 if self.notifications.is_empty() {
3557 None
3558 } else {
3559 Some(
3560 div()
3561 .absolute()
3562 .right_3()
3563 .bottom_3()
3564 .w_112()
3565 .h_full()
3566 .flex()
3567 .flex_col()
3568 .justify_end()
3569 .gap_2()
3570 .children(
3571 self.notifications
3572 .iter()
3573 .map(|(_, notification)| notification.to_any()),
3574 ),
3575 )
3576 }
3577 }
3578
3579 // RPC handlers
3580
3581 fn active_view_for_follower(
3582 &self,
3583 follower_project_id: Option<u64>,
3584 cx: &mut ViewContext<Self>,
3585 ) -> Option<proto::View> {
3586 let (item, panel_id) = self.active_item_for_followers(cx);
3587 let item = item?;
3588 let leader_id = self
3589 .pane_for(&*item)
3590 .and_then(|pane| self.leader_for_pane(&pane));
3591
3592 let item_handle = item.to_followable_item_handle(cx)?;
3593 let id = item_handle.remote_id(&self.app_state.client, cx)?;
3594 let variant = item_handle.to_state_proto(cx)?;
3595
3596 if item_handle.is_project_item(cx)
3597 && (follower_project_id.is_none()
3598 || follower_project_id != self.project.read(cx).remote_id())
3599 {
3600 return None;
3601 }
3602
3603 Some(proto::View {
3604 id: Some(id.to_proto()),
3605 leader_id,
3606 variant: Some(variant),
3607 panel_id: panel_id.map(|id| id as i32),
3608 })
3609 }
3610
3611 fn handle_follow(
3612 &mut self,
3613 follower_project_id: Option<u64>,
3614 cx: &mut ViewContext<Self>,
3615 ) -> proto::FollowResponse {
3616 let active_view = self.active_view_for_follower(follower_project_id, cx);
3617
3618 cx.notify();
3619 proto::FollowResponse {
3620 // TODO: Remove after version 0.145.x stabilizes.
3621 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3622 views: active_view.iter().cloned().collect(),
3623 active_view,
3624 }
3625 }
3626
3627 fn handle_update_followers(
3628 &mut self,
3629 leader_id: PeerId,
3630 message: proto::UpdateFollowers,
3631 _cx: &mut ViewContext<Self>,
3632 ) {
3633 self.leader_updates_tx
3634 .unbounded_send((leader_id, message))
3635 .ok();
3636 }
3637
3638 async fn process_leader_update(
3639 this: &WeakView<Self>,
3640 leader_id: PeerId,
3641 update: proto::UpdateFollowers,
3642 cx: &mut AsyncWindowContext,
3643 ) -> Result<()> {
3644 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3645 proto::update_followers::Variant::CreateView(view) => {
3646 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3647 let should_add_view = this.update(cx, |this, _| {
3648 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3649 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3650 } else {
3651 anyhow::Ok(false)
3652 }
3653 })??;
3654
3655 if should_add_view {
3656 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3657 }
3658 }
3659 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3660 let should_add_view = this.update(cx, |this, _| {
3661 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3662 state.active_view_id = update_active_view
3663 .view
3664 .as_ref()
3665 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3666
3667 if state.active_view_id.is_some_and(|view_id| {
3668 !state.items_by_leader_view_id.contains_key(&view_id)
3669 }) {
3670 anyhow::Ok(true)
3671 } else {
3672 anyhow::Ok(false)
3673 }
3674 } else {
3675 anyhow::Ok(false)
3676 }
3677 })??;
3678
3679 if should_add_view {
3680 if let Some(view) = update_active_view.view {
3681 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3682 }
3683 }
3684 }
3685 proto::update_followers::Variant::UpdateView(update_view) => {
3686 let variant = update_view
3687 .variant
3688 .ok_or_else(|| anyhow!("missing update view variant"))?;
3689 let id = update_view
3690 .id
3691 .ok_or_else(|| anyhow!("missing update view id"))?;
3692 let mut tasks = Vec::new();
3693 this.update(cx, |this, cx| {
3694 let project = this.project.clone();
3695 if let Some(state) = this.follower_states.get(&leader_id) {
3696 let view_id = ViewId::from_proto(id.clone())?;
3697 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3698 tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3699 }
3700 }
3701 anyhow::Ok(())
3702 })??;
3703 try_join_all(tasks).await.log_err();
3704 }
3705 }
3706 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3707 Ok(())
3708 }
3709
3710 async fn add_view_from_leader(
3711 this: WeakView<Self>,
3712 leader_id: PeerId,
3713 view: &proto::View,
3714 cx: &mut AsyncWindowContext,
3715 ) -> Result<()> {
3716 let this = this.upgrade().context("workspace dropped")?;
3717
3718 let Some(id) = view.id.clone() else {
3719 return Err(anyhow!("no id for view"));
3720 };
3721 let id = ViewId::from_proto(id)?;
3722 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
3723
3724 let pane = this.update(cx, |this, _cx| {
3725 let state = this
3726 .follower_states
3727 .get(&leader_id)
3728 .context("stopped following")?;
3729 anyhow::Ok(state.pane().clone())
3730 })??;
3731 let existing_item = pane.update(cx, |pane, cx| {
3732 let client = this.read(cx).client().clone();
3733 pane.items().find_map(|item| {
3734 let item = item.to_followable_item_handle(cx)?;
3735 if item.remote_id(&client, cx) == Some(id) {
3736 Some(item)
3737 } else {
3738 None
3739 }
3740 })
3741 })?;
3742 let item = if let Some(existing_item) = existing_item {
3743 existing_item
3744 } else {
3745 let variant = view.variant.clone();
3746 if variant.is_none() {
3747 Err(anyhow!("missing view variant"))?;
3748 }
3749
3750 let task = cx.update(|cx| {
3751 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3752 })?;
3753
3754 let Some(task) = task else {
3755 return Err(anyhow!(
3756 "failed to construct view from leader (maybe from a different version of zed?)"
3757 ));
3758 };
3759
3760 let mut new_item = task.await?;
3761 pane.update(cx, |pane, cx| {
3762 let mut item_to_remove = None;
3763 for (ix, item) in pane.items().enumerate() {
3764 if let Some(item) = item.to_followable_item_handle(cx) {
3765 match new_item.dedup(item.as_ref(), cx) {
3766 Some(item::Dedup::KeepExisting) => {
3767 new_item =
3768 item.boxed_clone().to_followable_item_handle(cx).unwrap();
3769 break;
3770 }
3771 Some(item::Dedup::ReplaceExisting) => {
3772 item_to_remove = Some((ix, item.item_id()));
3773 break;
3774 }
3775 None => {}
3776 }
3777 }
3778 }
3779
3780 if let Some((ix, id)) = item_to_remove {
3781 pane.remove_item(id, false, false, cx);
3782 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3783 }
3784 })?;
3785
3786 new_item
3787 };
3788
3789 this.update(cx, |this, cx| {
3790 let state = this.follower_states.get_mut(&leader_id)?;
3791 item.set_leader_peer_id(Some(leader_id), cx);
3792 state.items_by_leader_view_id.insert(
3793 id,
3794 FollowerView {
3795 view: item,
3796 location: panel_id,
3797 },
3798 );
3799
3800 Some(())
3801 })?;
3802
3803 Ok(())
3804 }
3805
3806 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3807 let mut is_project_item = true;
3808 let mut update = proto::UpdateActiveView::default();
3809 if cx.is_window_active() {
3810 let (active_item, panel_id) = self.active_item_for_followers(cx);
3811
3812 if let Some(item) = active_item {
3813 if item.focus_handle(cx).contains_focused(cx) {
3814 let leader_id = self
3815 .pane_for(&*item)
3816 .and_then(|pane| self.leader_for_pane(&pane));
3817
3818 if let Some(item) = item.to_followable_item_handle(cx) {
3819 let id = item
3820 .remote_id(&self.app_state.client, cx)
3821 .map(|id| id.to_proto());
3822
3823 if let Some(id) = id.clone() {
3824 if let Some(variant) = item.to_state_proto(cx) {
3825 let view = Some(proto::View {
3826 id: Some(id.clone()),
3827 leader_id,
3828 variant: Some(variant),
3829 panel_id: panel_id.map(|id| id as i32),
3830 });
3831
3832 is_project_item = item.is_project_item(cx);
3833 update = proto::UpdateActiveView {
3834 view,
3835 // TODO: Remove after version 0.145.x stabilizes.
3836 id: Some(id.clone()),
3837 leader_id,
3838 };
3839 }
3840 };
3841 }
3842 }
3843 }
3844 }
3845
3846 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3847 if active_view_id != self.last_active_view_id.as_ref() {
3848 self.last_active_view_id = active_view_id.cloned();
3849 self.update_followers(
3850 is_project_item,
3851 proto::update_followers::Variant::UpdateActiveView(update),
3852 cx,
3853 );
3854 }
3855 }
3856
3857 fn active_item_for_followers(
3858 &self,
3859 cx: &mut WindowContext,
3860 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3861 let mut active_item = None;
3862 let mut panel_id = None;
3863 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3864 if dock.focus_handle(cx).contains_focused(cx) {
3865 if let Some(panel) = dock.read(cx).active_panel() {
3866 if let Some(pane) = panel.pane(cx) {
3867 if let Some(item) = pane.read(cx).active_item() {
3868 active_item = Some(item);
3869 panel_id = panel.remote_id();
3870 break;
3871 }
3872 }
3873 }
3874 }
3875 }
3876
3877 if active_item.is_none() {
3878 active_item = self.active_pane().read(cx).active_item();
3879 }
3880 (active_item, panel_id)
3881 }
3882
3883 fn update_followers(
3884 &self,
3885 project_only: bool,
3886 update: proto::update_followers::Variant,
3887 cx: &mut WindowContext,
3888 ) -> Option<()> {
3889 // If this update only applies to for followers in the current project,
3890 // then skip it unless this project is shared. If it applies to all
3891 // followers, regardless of project, then set `project_id` to none,
3892 // indicating that it goes to all followers.
3893 let project_id = if project_only {
3894 Some(self.project.read(cx).remote_id()?)
3895 } else {
3896 None
3897 };
3898 self.app_state().workspace_store.update(cx, |store, cx| {
3899 store.update_followers(project_id, update, cx)
3900 })
3901 }
3902
3903 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3904 self.follower_states.iter().find_map(|(leader_id, state)| {
3905 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3906 Some(*leader_id)
3907 } else {
3908 None
3909 }
3910 })
3911 }
3912
3913 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3914 cx.notify();
3915
3916 let call = self.active_call()?;
3917 let room = call.read(cx).room()?.read(cx);
3918 let participant = room.remote_participant_for_peer_id(leader_id)?;
3919
3920 let leader_in_this_app;
3921 let leader_in_this_project;
3922 match participant.location {
3923 call::ParticipantLocation::SharedProject { project_id } => {
3924 leader_in_this_app = true;
3925 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3926 }
3927 call::ParticipantLocation::UnsharedProject => {
3928 leader_in_this_app = true;
3929 leader_in_this_project = false;
3930 }
3931 call::ParticipantLocation::External => {
3932 leader_in_this_app = false;
3933 leader_in_this_project = false;
3934 }
3935 };
3936
3937 let state = self.follower_states.get(&leader_id)?;
3938 let mut item_to_activate = None;
3939 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3940 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3941 if leader_in_this_project || !item.view.is_project_item(cx) {
3942 item_to_activate = Some((item.location, item.view.boxed_clone()));
3943 }
3944 }
3945 } else if let Some(shared_screen) =
3946 self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
3947 {
3948 item_to_activate = Some((None, Box::new(shared_screen)));
3949 }
3950
3951 let (panel_id, item) = item_to_activate?;
3952
3953 let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
3954 let pane;
3955 if let Some(panel_id) = panel_id {
3956 pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
3957 let state = self.follower_states.get_mut(&leader_id)?;
3958 state.dock_pane = Some(pane.clone());
3959 } else {
3960 pane = state.center_pane.clone();
3961 let state = self.follower_states.get_mut(&leader_id)?;
3962 if let Some(dock_pane) = state.dock_pane.take() {
3963 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
3964 }
3965 }
3966
3967 pane.update(cx, |pane, cx| {
3968 let focus_active_item = pane.has_focus(cx) || transfer_focus;
3969 if let Some(index) = pane.index_for_item(item.as_ref()) {
3970 pane.activate_item(index, false, false, cx);
3971 } else {
3972 pane.add_item(item.boxed_clone(), false, false, None, cx)
3973 }
3974
3975 if focus_active_item {
3976 pane.focus_active_item(cx)
3977 }
3978 });
3979
3980 None
3981 }
3982
3983 #[cfg(target_os = "windows")]
3984 fn shared_screen_for_peer(
3985 &self,
3986 _peer_id: PeerId,
3987 _pane: &View<Pane>,
3988 _cx: &mut WindowContext,
3989 ) -> Option<View<SharedScreen>> {
3990 None
3991 }
3992
3993 #[cfg(not(target_os = "windows"))]
3994 fn shared_screen_for_peer(
3995 &self,
3996 peer_id: PeerId,
3997 pane: &View<Pane>,
3998 cx: &mut WindowContext,
3999 ) -> Option<View<SharedScreen>> {
4000 let call = self.active_call()?;
4001 let room = call.read(cx).room()?.read(cx);
4002 let participant = room.remote_participant_for_peer_id(peer_id)?;
4003 let track = participant.video_tracks.values().next()?.clone();
4004 let user = participant.user.clone();
4005
4006 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4007 if item.read(cx).peer_id == peer_id {
4008 return Some(item);
4009 }
4010 }
4011
4012 Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx)))
4013 }
4014
4015 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
4016 if cx.is_window_active() {
4017 self.update_active_view_for_followers(cx);
4018
4019 if let Some(database_id) = self.database_id {
4020 cx.background_executor()
4021 .spawn(persistence::DB.update_timestamp(database_id))
4022 .detach();
4023 }
4024 } else {
4025 for pane in &self.panes {
4026 pane.update(cx, |pane, cx| {
4027 if let Some(item) = pane.active_item() {
4028 item.workspace_deactivated(cx);
4029 }
4030 for item in pane.items() {
4031 if matches!(
4032 item.workspace_settings(cx).autosave,
4033 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4034 ) {
4035 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
4036 .detach_and_log_err(cx);
4037 }
4038 }
4039 });
4040 }
4041 }
4042 }
4043
4044 fn active_call(&self) -> Option<&Model<ActiveCall>> {
4045 self.active_call.as_ref().map(|(call, _)| call)
4046 }
4047
4048 fn on_active_call_event(
4049 &mut self,
4050 _: Model<ActiveCall>,
4051 event: &call::room::Event,
4052 cx: &mut ViewContext<Self>,
4053 ) {
4054 match event {
4055 call::room::Event::ParticipantLocationChanged { participant_id }
4056 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4057 self.leader_updated(*participant_id, cx);
4058 }
4059 _ => {}
4060 }
4061 }
4062
4063 pub fn database_id(&self) -> Option<WorkspaceId> {
4064 self.database_id
4065 }
4066
4067 fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
4068 let project = self.project().read(cx);
4069
4070 if project.is_local() {
4071 Some(
4072 project
4073 .visible_worktrees(cx)
4074 .map(|worktree| worktree.read(cx).abs_path())
4075 .collect::<Vec<_>>(),
4076 )
4077 } else {
4078 None
4079 }
4080 }
4081
4082 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
4083 match member {
4084 Member::Axis(PaneAxis { members, .. }) => {
4085 for child in members.iter() {
4086 self.remove_panes(child.clone(), cx)
4087 }
4088 }
4089 Member::Pane(pane) => {
4090 self.force_remove_pane(&pane, &None, cx);
4091 }
4092 }
4093 }
4094
4095 fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
4096 self.session_id.take();
4097 self.serialize_workspace_internal(cx)
4098 }
4099
4100 fn force_remove_pane(
4101 &mut self,
4102 pane: &View<Pane>,
4103 focus_on: &Option<View<Pane>>,
4104 cx: &mut ViewContext<Workspace>,
4105 ) {
4106 self.panes.retain(|p| p != pane);
4107 if let Some(focus_on) = focus_on {
4108 focus_on.update(cx, |pane, cx| pane.focus(cx));
4109 } else {
4110 self.panes
4111 .last()
4112 .unwrap()
4113 .update(cx, |pane, cx| pane.focus(cx));
4114 }
4115 if self.last_active_center_pane == Some(pane.downgrade()) {
4116 self.last_active_center_pane = None;
4117 }
4118 cx.notify();
4119 }
4120
4121 fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4122 if self._schedule_serialize.is_none() {
4123 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4124 cx.background_executor()
4125 .timer(Duration::from_millis(100))
4126 .await;
4127 this.update(&mut cx, |this, cx| {
4128 this.serialize_workspace_internal(cx).detach();
4129 this._schedule_serialize.take();
4130 })
4131 .log_err();
4132 }));
4133 }
4134 }
4135
4136 fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4137 let Some(database_id) = self.database_id() else {
4138 return Task::ready(());
4139 };
4140
4141 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4142 let (items, active, pinned_count) = {
4143 let pane = pane_handle.read(cx);
4144 let active_item_id = pane.active_item().map(|item| item.item_id());
4145 (
4146 pane.items()
4147 .filter_map(|handle| {
4148 let handle = handle.to_serializable_item_handle(cx)?;
4149
4150 Some(SerializedItem {
4151 kind: Arc::from(handle.serialized_item_kind()),
4152 item_id: handle.item_id().as_u64(),
4153 active: Some(handle.item_id()) == active_item_id,
4154 preview: pane.is_active_preview_item(handle.item_id()),
4155 })
4156 })
4157 .collect::<Vec<_>>(),
4158 pane.has_focus(cx),
4159 pane.pinned_count(),
4160 )
4161 };
4162
4163 SerializedPane::new(items, active, pinned_count)
4164 }
4165
4166 fn build_serialized_pane_group(
4167 pane_group: &Member,
4168 cx: &WindowContext,
4169 ) -> SerializedPaneGroup {
4170 match pane_group {
4171 Member::Axis(PaneAxis {
4172 axis,
4173 members,
4174 flexes,
4175 bounding_boxes: _,
4176 }) => SerializedPaneGroup::Group {
4177 axis: SerializedAxis(*axis),
4178 children: members
4179 .iter()
4180 .map(|member| build_serialized_pane_group(member, cx))
4181 .collect::<Vec<_>>(),
4182 flexes: Some(flexes.lock().clone()),
4183 },
4184 Member::Pane(pane_handle) => {
4185 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4186 }
4187 }
4188 }
4189
4190 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4191 let left_dock = this.left_dock.read(cx);
4192 let left_visible = left_dock.is_open();
4193 let left_active_panel = left_dock
4194 .active_panel()
4195 .map(|panel| panel.persistent_name().to_string());
4196 let left_dock_zoom = left_dock
4197 .active_panel()
4198 .map(|panel| panel.is_zoomed(cx))
4199 .unwrap_or(false);
4200
4201 let right_dock = this.right_dock.read(cx);
4202 let right_visible = right_dock.is_open();
4203 let right_active_panel = right_dock
4204 .active_panel()
4205 .map(|panel| panel.persistent_name().to_string());
4206 let right_dock_zoom = right_dock
4207 .active_panel()
4208 .map(|panel| panel.is_zoomed(cx))
4209 .unwrap_or(false);
4210
4211 let bottom_dock = this.bottom_dock.read(cx);
4212 let bottom_visible = bottom_dock.is_open();
4213 let bottom_active_panel = bottom_dock
4214 .active_panel()
4215 .map(|panel| panel.persistent_name().to_string());
4216 let bottom_dock_zoom = bottom_dock
4217 .active_panel()
4218 .map(|panel| panel.is_zoomed(cx))
4219 .unwrap_or(false);
4220
4221 DockStructure {
4222 left: DockData {
4223 visible: left_visible,
4224 active_panel: left_active_panel,
4225 zoom: left_dock_zoom,
4226 },
4227 right: DockData {
4228 visible: right_visible,
4229 active_panel: right_active_panel,
4230 zoom: right_dock_zoom,
4231 },
4232 bottom: DockData {
4233 visible: bottom_visible,
4234 active_panel: bottom_active_panel,
4235 zoom: bottom_dock_zoom,
4236 },
4237 }
4238 }
4239
4240 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4241 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4242 } else if let Some(local_paths) = self.local_paths(cx) {
4243 if !local_paths.is_empty() {
4244 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4245 } else {
4246 None
4247 }
4248 } else {
4249 None
4250 };
4251
4252 if let Some(location) = location {
4253 let center_group = build_serialized_pane_group(&self.center.root, cx);
4254 let docks = build_serialized_docks(self, cx);
4255 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4256 let serialized_workspace = SerializedWorkspace {
4257 id: database_id,
4258 location,
4259 center_group,
4260 window_bounds,
4261 display: Default::default(),
4262 docks,
4263 centered_layout: self.centered_layout,
4264 session_id: self.session_id.clone(),
4265 window_id: Some(cx.window_handle().window_id().as_u64()),
4266 };
4267 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4268 }
4269 Task::ready(())
4270 }
4271
4272 async fn serialize_items(
4273 this: &WeakView<Self>,
4274 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4275 cx: &mut AsyncWindowContext,
4276 ) -> Result<()> {
4277 const CHUNK_SIZE: usize = 200;
4278 const THROTTLE_TIME: Duration = Duration::from_millis(200);
4279
4280 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4281
4282 while let Some(items_received) = serializable_items.next().await {
4283 let unique_items =
4284 items_received
4285 .into_iter()
4286 .fold(HashMap::default(), |mut acc, item| {
4287 acc.entry(item.item_id()).or_insert(item);
4288 acc
4289 });
4290
4291 // We use into_iter() here so that the references to the items are moved into
4292 // the tasks and not kept alive while we're sleeping.
4293 for (_, item) in unique_items.into_iter() {
4294 if let Ok(Some(task)) =
4295 this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4296 {
4297 cx.background_executor()
4298 .spawn(async move { task.await.log_err() })
4299 .detach();
4300 }
4301 }
4302
4303 cx.background_executor().timer(THROTTLE_TIME).await;
4304 }
4305
4306 Ok(())
4307 }
4308
4309 pub(crate) fn enqueue_item_serialization(
4310 &mut self,
4311 item: Box<dyn SerializableItemHandle>,
4312 ) -> Result<()> {
4313 self.serializable_items_tx
4314 .unbounded_send(item)
4315 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4316 }
4317
4318 pub(crate) fn load_workspace(
4319 serialized_workspace: SerializedWorkspace,
4320 paths_to_open: Vec<Option<ProjectPath>>,
4321 cx: &mut ViewContext<Workspace>,
4322 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4323 cx.spawn(|workspace, mut cx| async move {
4324 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4325
4326 let mut center_group = None;
4327 let mut center_items = None;
4328
4329 // Traverse the splits tree and add to things
4330 if let Some((group, active_pane, items)) = serialized_workspace
4331 .center_group
4332 .deserialize(
4333 &project,
4334 serialized_workspace.id,
4335 workspace.clone(),
4336 &mut cx,
4337 )
4338 .await
4339 {
4340 center_items = Some(items);
4341 center_group = Some((group, active_pane))
4342 }
4343
4344 let mut items_by_project_path = HashMap::default();
4345 let mut item_ids_by_kind = HashMap::default();
4346 let mut all_deserialized_items = Vec::default();
4347 cx.update(|cx| {
4348 for item in center_items.unwrap_or_default().into_iter().flatten() {
4349 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4350 item_ids_by_kind
4351 .entry(serializable_item_handle.serialized_item_kind())
4352 .or_insert(Vec::new())
4353 .push(item.item_id().as_u64() as ItemId);
4354 }
4355
4356 if let Some(project_path) = item.project_path(cx) {
4357 items_by_project_path.insert(project_path, item.clone());
4358 }
4359 all_deserialized_items.push(item);
4360 }
4361 })?;
4362
4363 let opened_items = paths_to_open
4364 .into_iter()
4365 .map(|path_to_open| {
4366 path_to_open
4367 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4368 })
4369 .collect::<Vec<_>>();
4370
4371 // Remove old panes from workspace panes list
4372 workspace.update(&mut cx, |workspace, cx| {
4373 if let Some((center_group, active_pane)) = center_group {
4374 workspace.remove_panes(workspace.center.root.clone(), cx);
4375
4376 // Swap workspace center group
4377 workspace.center = PaneGroup::with_root(center_group);
4378 if let Some(active_pane) = active_pane {
4379 workspace.set_active_pane(&active_pane, cx);
4380 cx.focus_self();
4381 } else {
4382 workspace.set_active_pane(&workspace.center.first_pane(), cx);
4383 }
4384 }
4385
4386 let docks = serialized_workspace.docks;
4387
4388 for (dock, serialized_dock) in [
4389 (&mut workspace.right_dock, docks.right),
4390 (&mut workspace.left_dock, docks.left),
4391 (&mut workspace.bottom_dock, docks.bottom),
4392 ]
4393 .iter_mut()
4394 {
4395 dock.update(cx, |dock, cx| {
4396 dock.serialized_dock = Some(serialized_dock.clone());
4397 dock.restore_state(cx);
4398 });
4399 }
4400
4401 cx.notify();
4402 })?;
4403
4404 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4405 // after loading the items, we might have different items and in order to avoid
4406 // the database filling up, we delete items that haven't been loaded now.
4407 //
4408 // The items that have been loaded, have been saved after they've been added to the workspace.
4409 let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4410 item_ids_by_kind
4411 .into_iter()
4412 .map(|(item_kind, loaded_items)| {
4413 SerializableItemRegistry::cleanup(
4414 item_kind,
4415 serialized_workspace.id,
4416 loaded_items,
4417 cx,
4418 )
4419 .log_err()
4420 })
4421 .collect::<Vec<_>>()
4422 })?;
4423
4424 futures::future::join_all(clean_up_tasks).await;
4425
4426 workspace
4427 .update(&mut cx, |workspace, cx| {
4428 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4429 workspace.serialize_workspace_internal(cx).detach();
4430
4431 // Ensure that we mark the window as edited if we did load dirty items
4432 workspace.update_window_edited(cx);
4433 })
4434 .ok();
4435
4436 Ok(opened_items)
4437 })
4438 }
4439
4440 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4441 self.add_workspace_actions_listeners(div, cx)
4442 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4443 .on_action(cx.listener(Self::close_all_items_and_panes))
4444 .on_action(cx.listener(Self::save_all))
4445 .on_action(cx.listener(Self::send_keystrokes))
4446 .on_action(cx.listener(Self::add_folder_to_project))
4447 .on_action(cx.listener(Self::follow_next_collaborator))
4448 .on_action(cx.listener(Self::close_window))
4449 .on_action(cx.listener(Self::activate_pane_at_index))
4450 .on_action(cx.listener(Self::move_item_to_pane_at_index))
4451 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4452 let pane = workspace.active_pane().clone();
4453 workspace.unfollow_in_pane(&pane, cx);
4454 }))
4455 .on_action(cx.listener(|workspace, action: &Save, cx| {
4456 workspace
4457 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4458 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4459 }))
4460 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4461 workspace
4462 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4463 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4464 }))
4465 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4466 workspace
4467 .save_active_item(SaveIntent::SaveAs, cx)
4468 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4469 }))
4470 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4471 workspace.activate_previous_pane(cx)
4472 }))
4473 .on_action(
4474 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4475 )
4476 .on_action(
4477 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4478 workspace.activate_pane_in_direction(action.0, cx)
4479 }),
4480 )
4481 .on_action(
4482 cx.listener(|workspace, action: &MoveItemToPaneInDirection, cx| {
4483 workspace.move_item_to_pane_in_direction(action, cx)
4484 }),
4485 )
4486 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4487 workspace.swap_pane_in_direction(action.0, cx)
4488 }))
4489 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4490 this.toggle_dock(DockPosition::Left, cx);
4491 }))
4492 .on_action(
4493 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4494 workspace.toggle_dock(DockPosition::Right, cx);
4495 }),
4496 )
4497 .on_action(
4498 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4499 workspace.toggle_dock(DockPosition::Bottom, cx);
4500 }),
4501 )
4502 .on_action(
4503 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4504 workspace.close_all_docks(cx);
4505 }),
4506 )
4507 .on_action(
4508 cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4509 workspace.clear_all_notifications(cx);
4510 }),
4511 )
4512 .on_action(
4513 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4514 workspace.reopen_closed_item(cx).detach();
4515 }),
4516 )
4517 .on_action(cx.listener(Workspace::toggle_centered_layout))
4518 }
4519
4520 #[cfg(any(test, feature = "test-support"))]
4521 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4522 use node_runtime::NodeRuntime;
4523 use session::Session;
4524
4525 let client = project.read(cx).client();
4526 let user_store = project.read(cx).user_store();
4527
4528 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4529 let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4530 cx.activate_window();
4531 let app_state = Arc::new(AppState {
4532 languages: project.read(cx).languages().clone(),
4533 workspace_store,
4534 client,
4535 user_store,
4536 fs: project.read(cx).fs().clone(),
4537 build_window_options: |_, _| Default::default(),
4538 node_runtime: NodeRuntime::unavailable(),
4539 session,
4540 });
4541 let workspace = Self::new(Default::default(), project, app_state, cx);
4542 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4543 workspace
4544 }
4545
4546 pub fn register_action<A: Action>(
4547 &mut self,
4548 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4549 ) -> &mut Self {
4550 let callback = Arc::new(callback);
4551
4552 self.workspace_actions.push(Box::new(move |div, cx| {
4553 let callback = callback.clone();
4554 div.on_action(
4555 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4556 )
4557 }));
4558 self
4559 }
4560
4561 fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4562 for action in self.workspace_actions.iter() {
4563 div = (action)(div, cx)
4564 }
4565 div
4566 }
4567
4568 pub fn has_active_modal(&self, cx: &WindowContext) -> bool {
4569 self.modal_layer.read(cx).has_active_modal()
4570 }
4571
4572 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
4573 self.modal_layer.read(cx).active_modal()
4574 }
4575
4576 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4577 where
4578 B: FnOnce(&mut ViewContext<V>) -> V,
4579 {
4580 self.modal_layer
4581 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4582 }
4583
4584 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4585 self.centered_layout = !self.centered_layout;
4586 if let Some(database_id) = self.database_id() {
4587 cx.background_executor()
4588 .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4589 .detach_and_log_err(cx);
4590 }
4591 cx.notify();
4592 }
4593
4594 fn adjust_padding(padding: Option<f32>) -> f32 {
4595 padding
4596 .unwrap_or(Self::DEFAULT_PADDING)
4597 .clamp(0.0, Self::MAX_PADDING)
4598 }
4599
4600 fn render_dock(
4601 &self,
4602 position: DockPosition,
4603 dock: &View<Dock>,
4604 cx: &WindowContext,
4605 ) -> Option<Div> {
4606 if self.zoomed_position == Some(position) {
4607 return None;
4608 }
4609
4610 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4611 let pane = panel.pane(cx)?;
4612 let follower_states = &self.follower_states;
4613 leader_border_for_pane(follower_states, &pane, cx)
4614 });
4615
4616 Some(
4617 div()
4618 .flex()
4619 .flex_none()
4620 .overflow_hidden()
4621 .child(dock.clone())
4622 .children(leader_border),
4623 )
4624 }
4625
4626 pub fn for_window(cx: &mut WindowContext) -> Option<View<Workspace>> {
4627 let window = cx.window_handle().downcast::<Workspace>()?;
4628 cx.read_window(&window, |workspace, _| workspace).ok()
4629 }
4630
4631 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
4632 self.zoomed.as_ref()
4633 }
4634}
4635
4636fn leader_border_for_pane(
4637 follower_states: &HashMap<PeerId, FollowerState>,
4638 pane: &View<Pane>,
4639 cx: &WindowContext,
4640) -> Option<Div> {
4641 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4642 if state.pane() == pane {
4643 Some((*leader_id, state))
4644 } else {
4645 None
4646 }
4647 })?;
4648
4649 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4650 let leader = room.remote_participant_for_peer_id(leader_id)?;
4651
4652 let mut leader_color = cx
4653 .theme()
4654 .players()
4655 .color_for_participant(leader.participant_index.0)
4656 .cursor;
4657 leader_color.fade_out(0.3);
4658 Some(
4659 div()
4660 .absolute()
4661 .size_full()
4662 .left_0()
4663 .top_0()
4664 .border_2()
4665 .border_color(leader_color),
4666 )
4667}
4668
4669fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4670 ZED_WINDOW_POSITION
4671 .zip(*ZED_WINDOW_SIZE)
4672 .map(|(position, size)| Bounds {
4673 origin: position,
4674 size,
4675 })
4676}
4677
4678fn open_items(
4679 serialized_workspace: Option<SerializedWorkspace>,
4680 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4681 cx: &mut ViewContext<Workspace>,
4682) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4683 let restored_items = serialized_workspace.map(|serialized_workspace| {
4684 Workspace::load_workspace(
4685 serialized_workspace,
4686 project_paths_to_open
4687 .iter()
4688 .map(|(_, project_path)| project_path)
4689 .cloned()
4690 .collect(),
4691 cx,
4692 )
4693 });
4694
4695 cx.spawn(|workspace, mut cx| async move {
4696 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4697
4698 if let Some(restored_items) = restored_items {
4699 let restored_items = restored_items.await?;
4700
4701 let restored_project_paths = restored_items
4702 .iter()
4703 .filter_map(|item| {
4704 cx.update(|cx| item.as_ref()?.project_path(cx))
4705 .ok()
4706 .flatten()
4707 })
4708 .collect::<HashSet<_>>();
4709
4710 for restored_item in restored_items {
4711 opened_items.push(restored_item.map(Ok));
4712 }
4713
4714 project_paths_to_open
4715 .iter_mut()
4716 .for_each(|(_, project_path)| {
4717 if let Some(project_path_to_open) = project_path {
4718 if restored_project_paths.contains(project_path_to_open) {
4719 *project_path = None;
4720 }
4721 }
4722 });
4723 } else {
4724 for _ in 0..project_paths_to_open.len() {
4725 opened_items.push(None);
4726 }
4727 }
4728 assert!(opened_items.len() == project_paths_to_open.len());
4729
4730 let tasks =
4731 project_paths_to_open
4732 .into_iter()
4733 .enumerate()
4734 .map(|(ix, (abs_path, project_path))| {
4735 let workspace = workspace.clone();
4736 cx.spawn(|mut cx| async move {
4737 let file_project_path = project_path?;
4738 let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4739 workspace.project().update(cx, |project, cx| {
4740 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4741 })
4742 });
4743
4744 // We only want to open file paths here. If one of the items
4745 // here is a directory, it was already opened further above
4746 // with a `find_or_create_worktree`.
4747 if let Ok(task) = abs_path_task {
4748 if task.await.map_or(true, |p| p.is_file()) {
4749 return Some((
4750 ix,
4751 workspace
4752 .update(&mut cx, |workspace, cx| {
4753 workspace.open_path(file_project_path, None, true, cx)
4754 })
4755 .log_err()?
4756 .await,
4757 ));
4758 }
4759 }
4760 None
4761 })
4762 });
4763
4764 let tasks = tasks.collect::<Vec<_>>();
4765
4766 let tasks = futures::future::join_all(tasks);
4767 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4768 opened_items[ix] = Some(path_open_result);
4769 }
4770
4771 Ok(opened_items)
4772 })
4773}
4774
4775enum ActivateInDirectionTarget {
4776 Pane(View<Pane>),
4777 Dock(View<Dock>),
4778}
4779
4780fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4781 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";
4782
4783 workspace
4784 .update(cx, |workspace, cx| {
4785 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4786 struct DatabaseFailedNotification;
4787
4788 workspace.show_notification_once(
4789 NotificationId::unique::<DatabaseFailedNotification>(),
4790 cx,
4791 |cx| {
4792 cx.new_view(|_| {
4793 MessageNotification::new("Failed to load the database file.")
4794 .with_click_message("File an issue")
4795 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4796 })
4797 },
4798 );
4799 }
4800 })
4801 .log_err();
4802}
4803
4804impl FocusableView for Workspace {
4805 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4806 self.active_pane.focus_handle(cx)
4807 }
4808}
4809
4810#[derive(Clone, Render)]
4811struct DraggedDock(DockPosition);
4812
4813impl Render for Workspace {
4814 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4815 let mut context = KeyContext::new_with_defaults();
4816 context.add("Workspace");
4817 context.set("keyboard_layout", cx.keyboard_layout().clone());
4818 let centered_layout = self.centered_layout
4819 && self.center.panes().len() == 1
4820 && self.active_item(cx).is_some();
4821 let render_padding = |size| {
4822 (size > 0.0).then(|| {
4823 div()
4824 .h_full()
4825 .w(relative(size))
4826 .bg(cx.theme().colors().editor_background)
4827 .border_color(cx.theme().colors().pane_group_border)
4828 })
4829 };
4830 let paddings = if centered_layout {
4831 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4832 (
4833 render_padding(Self::adjust_padding(settings.left_padding)),
4834 render_padding(Self::adjust_padding(settings.right_padding)),
4835 )
4836 } else {
4837 (None, None)
4838 };
4839 let ui_font = theme::setup_ui_font(cx);
4840
4841 let theme = cx.theme().clone();
4842 let colors = theme.colors();
4843
4844 client_side_decorations(
4845 self.actions(div(), cx)
4846 .key_context(context)
4847 .relative()
4848 .size_full()
4849 .flex()
4850 .flex_col()
4851 .font(ui_font)
4852 .gap_0()
4853 .justify_start()
4854 .items_start()
4855 .text_color(colors.text)
4856 .overflow_hidden()
4857 .children(self.titlebar_item.clone())
4858 .child(
4859 div()
4860 .size_full()
4861 .relative()
4862 .flex_1()
4863 .flex()
4864 .flex_col()
4865 .child(
4866 div()
4867 .id("workspace")
4868 .bg(colors.background)
4869 .relative()
4870 .flex_1()
4871 .w_full()
4872 .flex()
4873 .flex_col()
4874 .overflow_hidden()
4875 .border_t_1()
4876 .border_b_1()
4877 .border_color(colors.border)
4878 .child({
4879 let this = cx.view().clone();
4880 canvas(
4881 move |bounds, cx| {
4882 this.update(cx, |this, cx| {
4883 let bounds_changed = this.bounds != bounds;
4884 this.bounds = bounds;
4885
4886 if bounds_changed {
4887 this.left_dock.update(cx, |dock, cx| {
4888 dock.clamp_panel_size(bounds.size.width, cx)
4889 });
4890
4891 this.right_dock.update(cx, |dock, cx| {
4892 dock.clamp_panel_size(bounds.size.width, cx)
4893 });
4894
4895 this.bottom_dock.update(cx, |dock, cx| {
4896 dock.clamp_panel_size(
4897 bounds.size.height,
4898 cx,
4899 )
4900 });
4901 }
4902 })
4903 },
4904 |_, _, _| {},
4905 )
4906 .absolute()
4907 .size_full()
4908 })
4909 .when(self.zoomed.is_none(), |this| {
4910 this.on_drag_move(cx.listener(
4911 |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4912 match e.drag(cx).0 {
4913 DockPosition::Left => {
4914 resize_left_dock(
4915 e.event.position.x
4916 - workspace.bounds.left(),
4917 workspace,
4918 cx,
4919 );
4920 }
4921 DockPosition::Right => {
4922 resize_right_dock(
4923 workspace.bounds.right()
4924 - e.event.position.x,
4925 workspace,
4926 cx,
4927 );
4928 }
4929 DockPosition::Bottom => {
4930 resize_bottom_dock(
4931 workspace.bounds.bottom()
4932 - e.event.position.y,
4933 workspace,
4934 cx,
4935 );
4936 }
4937 }
4938 },
4939 ))
4940 })
4941 .child(
4942 div()
4943 .flex()
4944 .flex_row()
4945 .h_full()
4946 // Left Dock
4947 .children(self.render_dock(
4948 DockPosition::Left,
4949 &self.left_dock,
4950 cx,
4951 ))
4952 // Panes
4953 .child(
4954 div()
4955 .flex()
4956 .flex_col()
4957 .flex_1()
4958 .overflow_hidden()
4959 .child(
4960 h_flex()
4961 .flex_1()
4962 .when_some(paddings.0, |this, p| {
4963 this.child(p.border_r_1())
4964 })
4965 .child(self.center.render(
4966 &self.project,
4967 &self.follower_states,
4968 self.active_call(),
4969 &self.active_pane,
4970 self.zoomed.as_ref(),
4971 &self.app_state,
4972 cx,
4973 ))
4974 .when_some(paddings.1, |this, p| {
4975 this.child(p.border_l_1())
4976 }),
4977 )
4978 .children(self.render_dock(
4979 DockPosition::Bottom,
4980 &self.bottom_dock,
4981 cx,
4982 )),
4983 )
4984 // Right Dock
4985 .children(self.render_dock(
4986 DockPosition::Right,
4987 &self.right_dock,
4988 cx,
4989 )),
4990 )
4991 .children(self.zoomed.as_ref().and_then(|view| {
4992 let zoomed_view = view.upgrade()?;
4993 let div = div()
4994 .occlude()
4995 .absolute()
4996 .overflow_hidden()
4997 .border_color(colors.border)
4998 .bg(colors.background)
4999 .child(zoomed_view)
5000 .inset_0()
5001 .shadow_lg();
5002
5003 Some(match self.zoomed_position {
5004 Some(DockPosition::Left) => div.right_2().border_r_1(),
5005 Some(DockPosition::Right) => div.left_2().border_l_1(),
5006 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5007 None => {
5008 div.top_2().bottom_2().left_2().right_2().border_1()
5009 }
5010 })
5011 }))
5012 .children(self.render_notifications(cx)),
5013 )
5014 .child(self.status_bar.clone())
5015 .child(self.modal_layer.clone()),
5016 ),
5017 cx,
5018 )
5019 }
5020}
5021
5022fn resize_bottom_dock(
5023 new_size: Pixels,
5024 workspace: &mut Workspace,
5025 cx: &mut ViewContext<Workspace>,
5026) {
5027 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5028 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5029 bottom_dock.resize_active_panel(Some(size), cx);
5030 });
5031}
5032
5033fn resize_right_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
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(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5041 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5042
5043 workspace.left_dock.update(cx, |left_dock, cx| {
5044 left_dock.resize_active_panel(Some(size), cx);
5045 });
5046}
5047
5048impl WorkspaceStore {
5049 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
5050 Self {
5051 workspaces: Default::default(),
5052 _subscriptions: vec![
5053 client.add_request_handler(cx.weak_model(), Self::handle_follow),
5054 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
5055 ],
5056 client,
5057 }
5058 }
5059
5060 pub fn update_followers(
5061 &self,
5062 project_id: Option<u64>,
5063 update: proto::update_followers::Variant,
5064 cx: &AppContext,
5065 ) -> Option<()> {
5066 let active_call = ActiveCall::try_global(cx)?;
5067 let room_id = active_call.read(cx).room()?.read(cx).id();
5068 self.client
5069 .send(proto::UpdateFollowers {
5070 room_id,
5071 project_id,
5072 variant: Some(update),
5073 })
5074 .log_err()
5075 }
5076
5077 pub async fn handle_follow(
5078 this: Model<Self>,
5079 envelope: TypedEnvelope<proto::Follow>,
5080 mut cx: AsyncAppContext,
5081 ) -> Result<proto::FollowResponse> {
5082 this.update(&mut cx, |this, cx| {
5083 let follower = Follower {
5084 project_id: envelope.payload.project_id,
5085 peer_id: envelope.original_sender_id()?,
5086 };
5087
5088 let mut response = proto::FollowResponse::default();
5089 this.workspaces.retain(|workspace| {
5090 workspace
5091 .update(cx, |workspace, cx| {
5092 let handler_response = workspace.handle_follow(follower.project_id, cx);
5093 if let Some(active_view) = handler_response.active_view.clone() {
5094 if workspace.project.read(cx).remote_id() == follower.project_id {
5095 response.active_view = Some(active_view)
5096 }
5097 }
5098 })
5099 .is_ok()
5100 });
5101
5102 Ok(response)
5103 })?
5104 }
5105
5106 async fn handle_update_followers(
5107 this: Model<Self>,
5108 envelope: TypedEnvelope<proto::UpdateFollowers>,
5109 mut cx: AsyncAppContext,
5110 ) -> Result<()> {
5111 let leader_id = envelope.original_sender_id()?;
5112 let update = envelope.payload;
5113
5114 this.update(&mut cx, |this, cx| {
5115 this.workspaces.retain(|workspace| {
5116 workspace
5117 .update(cx, |workspace, cx| {
5118 let project_id = workspace.project.read(cx).remote_id();
5119 if update.project_id != project_id && update.project_id.is_some() {
5120 return;
5121 }
5122 workspace.handle_update_followers(leader_id, update.clone(), cx);
5123 })
5124 .is_ok()
5125 });
5126 Ok(())
5127 })?
5128 }
5129}
5130
5131impl ViewId {
5132 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5133 Ok(Self {
5134 creator: message
5135 .creator
5136 .ok_or_else(|| anyhow!("creator is missing"))?,
5137 id: message.id,
5138 })
5139 }
5140
5141 pub(crate) fn to_proto(self) -> proto::ViewId {
5142 proto::ViewId {
5143 creator: Some(self.creator),
5144 id: self.id,
5145 }
5146 }
5147}
5148
5149impl FollowerState {
5150 fn pane(&self) -> &View<Pane> {
5151 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5152 }
5153}
5154
5155pub trait WorkspaceHandle {
5156 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5157}
5158
5159impl WorkspaceHandle for View<Workspace> {
5160 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5161 self.read(cx)
5162 .worktrees(cx)
5163 .flat_map(|worktree| {
5164 let worktree_id = worktree.read(cx).id();
5165 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5166 worktree_id,
5167 path: f.path.clone(),
5168 })
5169 })
5170 .collect::<Vec<_>>()
5171 }
5172}
5173
5174impl std::fmt::Debug for OpenPaths {
5175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5176 f.debug_struct("OpenPaths")
5177 .field("paths", &self.paths)
5178 .finish()
5179 }
5180}
5181
5182pub fn activate_workspace_for_project(
5183 cx: &mut AppContext,
5184 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5185) -> Option<WindowHandle<Workspace>> {
5186 for window in cx.windows() {
5187 let Some(workspace) = window.downcast::<Workspace>() else {
5188 continue;
5189 };
5190
5191 let predicate = workspace
5192 .update(cx, |workspace, cx| {
5193 let project = workspace.project.read(cx);
5194 if predicate(project, cx) {
5195 cx.activate_window();
5196 true
5197 } else {
5198 false
5199 }
5200 })
5201 .log_err()
5202 .unwrap_or(false);
5203
5204 if predicate {
5205 return Some(workspace);
5206 }
5207 }
5208
5209 None
5210}
5211
5212pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5213 DB.last_workspace().await.log_err().flatten()
5214}
5215
5216pub fn last_session_workspace_locations(
5217 last_session_id: &str,
5218 last_session_window_stack: Option<Vec<WindowId>>,
5219) -> Option<Vec<SerializedWorkspaceLocation>> {
5220 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5221 .log_err()
5222}
5223
5224actions!(collab, [OpenChannelNotes]);
5225actions!(zed, [OpenLog]);
5226
5227async fn join_channel_internal(
5228 channel_id: ChannelId,
5229 app_state: &Arc<AppState>,
5230 requesting_window: Option<WindowHandle<Workspace>>,
5231 active_call: &Model<ActiveCall>,
5232 cx: &mut AsyncAppContext,
5233) -> Result<bool> {
5234 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5235 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5236 return (false, None);
5237 };
5238
5239 let already_in_channel = room.channel_id() == Some(channel_id);
5240 let should_prompt = room.is_sharing_project()
5241 && !room.remote_participants().is_empty()
5242 && !already_in_channel;
5243 let open_room = if already_in_channel {
5244 active_call.room().cloned()
5245 } else {
5246 None
5247 };
5248 (should_prompt, open_room)
5249 })?;
5250
5251 if let Some(room) = open_room {
5252 let task = room.update(cx, |room, cx| {
5253 if let Some((project, host)) = room.most_active_project(cx) {
5254 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5255 }
5256
5257 None
5258 })?;
5259 if let Some(task) = task {
5260 task.await?;
5261 }
5262 return anyhow::Ok(true);
5263 }
5264
5265 if should_prompt {
5266 if let Some(workspace) = requesting_window {
5267 let answer = workspace
5268 .update(cx, |_, cx| {
5269 cx.prompt(
5270 PromptLevel::Warning,
5271 "Do you want to switch channels?",
5272 Some("Leaving this call will unshare your current project."),
5273 &["Yes, Join Channel", "Cancel"],
5274 )
5275 })?
5276 .await;
5277
5278 if answer == Ok(1) {
5279 return Ok(false);
5280 }
5281 } else {
5282 return Ok(false); // unreachable!() hopefully
5283 }
5284 }
5285
5286 let client = cx.update(|cx| active_call.read(cx).client())?;
5287
5288 let mut client_status = client.status();
5289
5290 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5291 'outer: loop {
5292 let Some(status) = client_status.recv().await else {
5293 return Err(anyhow!("error connecting"));
5294 };
5295
5296 match status {
5297 Status::Connecting
5298 | Status::Authenticating
5299 | Status::Reconnecting
5300 | Status::Reauthenticating => continue,
5301 Status::Connected { .. } => break 'outer,
5302 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5303 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5304 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5305 return Err(ErrorCode::Disconnected.into());
5306 }
5307 }
5308 }
5309
5310 let room = active_call
5311 .update(cx, |active_call, cx| {
5312 active_call.join_channel(channel_id, cx)
5313 })?
5314 .await?;
5315
5316 let Some(room) = room else {
5317 return anyhow::Ok(true);
5318 };
5319
5320 room.update(cx, |room, _| room.room_update_completed())?
5321 .await;
5322
5323 let task = room.update(cx, |room, cx| {
5324 if let Some((project, host)) = room.most_active_project(cx) {
5325 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5326 }
5327
5328 // If you are the first to join a channel, see if you should share your project.
5329 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5330 if let Some(workspace) = requesting_window {
5331 let project = workspace.update(cx, |workspace, cx| {
5332 let project = workspace.project.read(cx);
5333
5334 if !CallSettings::get_global(cx).share_on_join {
5335 return None;
5336 }
5337
5338 if (project.is_local() || project.is_via_ssh())
5339 && project.visible_worktrees(cx).any(|tree| {
5340 tree.read(cx)
5341 .root_entry()
5342 .map_or(false, |entry| entry.is_dir())
5343 })
5344 {
5345 Some(workspace.project.clone())
5346 } else {
5347 None
5348 }
5349 });
5350 if let Ok(Some(project)) = project {
5351 return Some(cx.spawn(|room, mut cx| async move {
5352 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5353 .await?;
5354 Ok(())
5355 }));
5356 }
5357 }
5358 }
5359
5360 None
5361 })?;
5362 if let Some(task) = task {
5363 task.await?;
5364 return anyhow::Ok(true);
5365 }
5366 anyhow::Ok(false)
5367}
5368
5369pub fn join_channel(
5370 channel_id: ChannelId,
5371 app_state: Arc<AppState>,
5372 requesting_window: Option<WindowHandle<Workspace>>,
5373 cx: &mut AppContext,
5374) -> Task<Result<()>> {
5375 let active_call = ActiveCall::global(cx);
5376 cx.spawn(|mut cx| async move {
5377 let result = join_channel_internal(
5378 channel_id,
5379 &app_state,
5380 requesting_window,
5381 &active_call,
5382 &mut cx,
5383 )
5384 .await;
5385
5386 // join channel succeeded, and opened a window
5387 if matches!(result, Ok(true)) {
5388 return anyhow::Ok(());
5389 }
5390
5391 // find an existing workspace to focus and show call controls
5392 let mut active_window =
5393 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5394 if active_window.is_none() {
5395 // no open workspaces, make one to show the error in (blergh)
5396 let (window_handle, _) = cx
5397 .update(|cx| {
5398 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5399 })?
5400 .await?;
5401
5402 if result.is_ok() {
5403 cx.update(|cx| {
5404 cx.dispatch_action(&OpenChannelNotes);
5405 }).log_err();
5406 }
5407
5408 active_window = Some(window_handle);
5409 }
5410
5411 if let Err(err) = result {
5412 log::error!("failed to join channel: {}", err);
5413 if let Some(active_window) = active_window {
5414 active_window
5415 .update(&mut cx, |_, cx| {
5416 let detail: SharedString = match err.error_code() {
5417 ErrorCode::SignedOut => {
5418 "Please sign in to continue.".into()
5419 }
5420 ErrorCode::UpgradeRequired => {
5421 "Your are running an unsupported version of Zed. Please update to continue.".into()
5422 }
5423 ErrorCode::NoSuchChannel => {
5424 "No matching channel was found. Please check the link and try again.".into()
5425 }
5426 ErrorCode::Forbidden => {
5427 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5428 }
5429 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5430 _ => format!("{}\n\nPlease try again.", err).into(),
5431 };
5432 cx.prompt(
5433 PromptLevel::Critical,
5434 "Failed to join channel",
5435 Some(&detail),
5436 &["Ok"],
5437 )
5438 })?
5439 .await
5440 .ok();
5441 }
5442 }
5443
5444 // return ok, we showed the error to the user.
5445 anyhow::Ok(())
5446 })
5447}
5448
5449pub async fn get_any_active_workspace(
5450 app_state: Arc<AppState>,
5451 mut cx: AsyncAppContext,
5452) -> anyhow::Result<WindowHandle<Workspace>> {
5453 // find an existing workspace to focus and show call controls
5454 let active_window = activate_any_workspace_window(&mut cx);
5455 if active_window.is_none() {
5456 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5457 .await?;
5458 }
5459 activate_any_workspace_window(&mut cx).context("could not open zed")
5460}
5461
5462fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5463 cx.update(|cx| {
5464 if let Some(workspace_window) = cx
5465 .active_window()
5466 .and_then(|window| window.downcast::<Workspace>())
5467 {
5468 return Some(workspace_window);
5469 }
5470
5471 for window in cx.windows() {
5472 if let Some(workspace_window) = window.downcast::<Workspace>() {
5473 workspace_window
5474 .update(cx, |_, cx| cx.activate_window())
5475 .ok();
5476 return Some(workspace_window);
5477 }
5478 }
5479 None
5480 })
5481 .ok()
5482 .flatten()
5483}
5484
5485pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5486 cx.windows()
5487 .into_iter()
5488 .filter_map(|window| window.downcast::<Workspace>())
5489 .filter(|workspace| {
5490 workspace
5491 .read(cx)
5492 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5493 })
5494 .collect()
5495}
5496
5497#[derive(Default)]
5498pub struct OpenOptions {
5499 pub open_new_workspace: Option<bool>,
5500 pub replace_window: Option<WindowHandle<Workspace>>,
5501 pub env: Option<HashMap<String, String>>,
5502}
5503
5504#[allow(clippy::type_complexity)]
5505pub fn open_paths(
5506 abs_paths: &[PathBuf],
5507 app_state: Arc<AppState>,
5508 open_options: OpenOptions,
5509 cx: &mut AppContext,
5510) -> Task<
5511 anyhow::Result<(
5512 WindowHandle<Workspace>,
5513 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5514 )>,
5515> {
5516 let abs_paths = abs_paths.to_vec();
5517 let mut existing = None;
5518 let mut best_match = None;
5519 let mut open_visible = OpenVisible::All;
5520
5521 if open_options.open_new_workspace != Some(true) {
5522 for window in local_workspace_windows(cx) {
5523 if let Ok(workspace) = window.read(cx) {
5524 let m = workspace
5525 .project
5526 .read(cx)
5527 .visibility_for_paths(&abs_paths, cx);
5528 if m > best_match {
5529 existing = Some(window);
5530 best_match = m;
5531 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5532 existing = Some(window)
5533 }
5534 }
5535 }
5536 }
5537
5538 cx.spawn(move |mut cx| async move {
5539 if open_options.open_new_workspace.is_none() && existing.is_none() {
5540 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5541 if futures::future::join_all(all_files)
5542 .await
5543 .into_iter()
5544 .filter_map(|result| result.ok().flatten())
5545 .all(|file| !file.is_dir)
5546 {
5547 cx.update(|cx| {
5548 for window in local_workspace_windows(cx) {
5549 if let Ok(workspace) = window.read(cx) {
5550 let project = workspace.project().read(cx);
5551 if project.is_via_collab() {
5552 continue;
5553 }
5554 existing = Some(window);
5555 open_visible = OpenVisible::None;
5556 break;
5557 }
5558 }
5559 })?;
5560 }
5561 }
5562
5563 if let Some(existing) = existing {
5564 Ok((
5565 existing,
5566 existing
5567 .update(&mut cx, |workspace, cx| {
5568 cx.activate_window();
5569 workspace.open_paths(abs_paths, open_visible, None, cx)
5570 })?
5571 .await,
5572 ))
5573 } else {
5574 cx.update(move |cx| {
5575 Workspace::new_local(
5576 abs_paths,
5577 app_state.clone(),
5578 open_options.replace_window,
5579 open_options.env,
5580 cx,
5581 )
5582 })?
5583 .await
5584 }
5585 })
5586}
5587
5588pub fn open_new(
5589 open_options: OpenOptions,
5590 app_state: Arc<AppState>,
5591 cx: &mut AppContext,
5592 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5593) -> Task<anyhow::Result<()>> {
5594 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5595 cx.spawn(|mut cx| async move {
5596 let (workspace, opened_paths) = task.await?;
5597 workspace.update(&mut cx, |workspace, cx| {
5598 if opened_paths.is_empty() {
5599 init(workspace, cx)
5600 }
5601 })?;
5602 Ok(())
5603 })
5604}
5605
5606pub fn create_and_open_local_file(
5607 path: &'static Path,
5608 cx: &mut ViewContext<Workspace>,
5609 default_content: impl 'static + Send + FnOnce() -> Rope,
5610) -> Task<Result<Box<dyn ItemHandle>>> {
5611 cx.spawn(|workspace, mut cx| async move {
5612 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5613 if !fs.is_file(path).await {
5614 fs.create_file(path, Default::default()).await?;
5615 fs.save(path, &default_content(), Default::default())
5616 .await?;
5617 }
5618
5619 let mut items = workspace
5620 .update(&mut cx, |workspace, cx| {
5621 workspace.with_local_workspace(cx, |workspace, cx| {
5622 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5623 })
5624 })?
5625 .await?
5626 .await;
5627
5628 let item = items.pop().flatten();
5629 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5630 })
5631}
5632
5633pub fn open_ssh_project(
5634 window: WindowHandle<Workspace>,
5635 connection_options: SshConnectionOptions,
5636 cancel_rx: oneshot::Receiver<()>,
5637 delegate: Arc<dyn SshClientDelegate>,
5638 app_state: Arc<AppState>,
5639 paths: Vec<PathBuf>,
5640 cx: &mut AppContext,
5641) -> Task<Result<()>> {
5642 cx.spawn(|mut cx| async move {
5643 let (serialized_ssh_project, workspace_id, serialized_workspace) =
5644 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5645
5646 let session = match cx
5647 .update(|cx| {
5648 remote::SshRemoteClient::new(
5649 ConnectionIdentifier::Workspace(workspace_id.0),
5650 connection_options,
5651 cancel_rx,
5652 delegate,
5653 cx,
5654 )
5655 })?
5656 .await?
5657 {
5658 Some(result) => result,
5659 None => return Ok(()),
5660 };
5661
5662 let project = cx.update(|cx| {
5663 project::Project::ssh(
5664 session,
5665 app_state.client.clone(),
5666 app_state.node_runtime.clone(),
5667 app_state.user_store.clone(),
5668 app_state.languages.clone(),
5669 app_state.fs.clone(),
5670 cx,
5671 )
5672 })?;
5673
5674 let toolchains = DB.toolchains(workspace_id).await?;
5675 for (toolchain, worktree_id) in toolchains {
5676 project
5677 .update(&mut cx, |this, cx| {
5678 this.activate_toolchain(worktree_id, toolchain, cx)
5679 })?
5680 .await;
5681 }
5682 let mut project_paths_to_open = vec![];
5683 let mut project_path_errors = vec![];
5684
5685 for path in paths {
5686 let result = cx
5687 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5688 .await;
5689 match result {
5690 Ok((_, project_path)) => {
5691 project_paths_to_open.push((path.clone(), Some(project_path)));
5692 }
5693 Err(error) => {
5694 project_path_errors.push(error);
5695 }
5696 };
5697 }
5698
5699 if project_paths_to_open.is_empty() {
5700 return Err(project_path_errors
5701 .pop()
5702 .unwrap_or_else(|| anyhow!("no paths given")));
5703 }
5704
5705 cx.update_window(window.into(), |_, cx| {
5706 cx.replace_root_view(|cx| {
5707 let mut workspace =
5708 Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5709
5710 workspace
5711 .client()
5712 .telemetry()
5713 .report_app_event("open ssh project".to_string());
5714
5715 workspace.set_serialized_ssh_project(serialized_ssh_project);
5716 workspace
5717 });
5718 })?;
5719
5720 window
5721 .update(&mut cx, |_, cx| {
5722 cx.activate_window();
5723
5724 open_items(serialized_workspace, project_paths_to_open, cx)
5725 })?
5726 .await?;
5727
5728 window.update(&mut cx, |workspace, cx| {
5729 for error in project_path_errors {
5730 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5731 if let Some(path) = error.error_tag("path") {
5732 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5733 }
5734 } else {
5735 workspace.show_error(&error, cx)
5736 }
5737 }
5738 })
5739 })
5740}
5741
5742fn serialize_ssh_project(
5743 connection_options: SshConnectionOptions,
5744 paths: Vec<PathBuf>,
5745 cx: &AsyncAppContext,
5746) -> Task<
5747 Result<(
5748 SerializedSshProject,
5749 WorkspaceId,
5750 Option<SerializedWorkspace>,
5751 )>,
5752> {
5753 cx.background_executor().spawn(async move {
5754 let serialized_ssh_project = persistence::DB
5755 .get_or_create_ssh_project(
5756 connection_options.host.clone(),
5757 connection_options.port,
5758 paths
5759 .iter()
5760 .map(|path| path.to_string_lossy().to_string())
5761 .collect::<Vec<_>>(),
5762 connection_options.username.clone(),
5763 )
5764 .await?;
5765
5766 let serialized_workspace =
5767 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5768
5769 let workspace_id = if let Some(workspace_id) =
5770 serialized_workspace.as_ref().map(|workspace| workspace.id)
5771 {
5772 workspace_id
5773 } else {
5774 persistence::DB.next_id().await?
5775 };
5776
5777 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5778 })
5779}
5780
5781pub fn join_in_room_project(
5782 project_id: u64,
5783 follow_user_id: u64,
5784 app_state: Arc<AppState>,
5785 cx: &mut AppContext,
5786) -> Task<Result<()>> {
5787 let windows = cx.windows();
5788 cx.spawn(|mut cx| async move {
5789 let existing_workspace = windows.into_iter().find_map(|window| {
5790 window.downcast::<Workspace>().and_then(|window| {
5791 window
5792 .update(&mut cx, |workspace, cx| {
5793 if workspace.project().read(cx).remote_id() == Some(project_id) {
5794 Some(window)
5795 } else {
5796 None
5797 }
5798 })
5799 .unwrap_or(None)
5800 })
5801 });
5802
5803 let workspace = if let Some(existing_workspace) = existing_workspace {
5804 existing_workspace
5805 } else {
5806 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5807 let room = active_call
5808 .read_with(&cx, |call, _| call.room().cloned())?
5809 .ok_or_else(|| anyhow!("not in a call"))?;
5810 let project = room
5811 .update(&mut cx, |room, cx| {
5812 room.join_project(
5813 project_id,
5814 app_state.languages.clone(),
5815 app_state.fs.clone(),
5816 cx,
5817 )
5818 })?
5819 .await?;
5820
5821 let window_bounds_override = window_bounds_env_override();
5822 cx.update(|cx| {
5823 let mut options = (app_state.build_window_options)(None, cx);
5824 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5825 cx.open_window(options, |cx| {
5826 cx.new_view(|cx| {
5827 Workspace::new(Default::default(), project, app_state.clone(), cx)
5828 })
5829 })
5830 })??
5831 };
5832
5833 workspace.update(&mut cx, |workspace, cx| {
5834 cx.activate(true);
5835 cx.activate_window();
5836
5837 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5838 let follow_peer_id = room
5839 .read(cx)
5840 .remote_participants()
5841 .iter()
5842 .find(|(_, participant)| participant.user.id == follow_user_id)
5843 .map(|(_, p)| p.peer_id)
5844 .or_else(|| {
5845 // If we couldn't follow the given user, follow the host instead.
5846 let collaborator = workspace
5847 .project()
5848 .read(cx)
5849 .collaborators()
5850 .values()
5851 .find(|collaborator| collaborator.is_host)?;
5852 Some(collaborator.peer_id)
5853 });
5854
5855 if let Some(follow_peer_id) = follow_peer_id {
5856 workspace.follow(follow_peer_id, cx);
5857 }
5858 }
5859 })?;
5860
5861 anyhow::Ok(())
5862 })
5863}
5864
5865pub fn reload(reload: &Reload, cx: &mut AppContext) {
5866 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5867 let mut workspace_windows = cx
5868 .windows()
5869 .into_iter()
5870 .filter_map(|window| window.downcast::<Workspace>())
5871 .collect::<Vec<_>>();
5872
5873 // If multiple windows have unsaved changes, and need a save prompt,
5874 // prompt in the active window before switching to a different window.
5875 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5876
5877 let mut prompt = None;
5878 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5879 prompt = window
5880 .update(cx, |_, cx| {
5881 cx.prompt(
5882 PromptLevel::Info,
5883 "Are you sure you want to restart?",
5884 None,
5885 &["Restart", "Cancel"],
5886 )
5887 })
5888 .ok();
5889 }
5890
5891 let binary_path = reload.binary_path.clone();
5892 cx.spawn(|mut cx| async move {
5893 if let Some(prompt) = prompt {
5894 let answer = prompt.await?;
5895 if answer != 0 {
5896 return Ok(());
5897 }
5898 }
5899
5900 // If the user cancels any save prompt, then keep the app open.
5901 for window in workspace_windows {
5902 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5903 workspace.prepare_to_close(CloseIntent::Quit, cx)
5904 }) {
5905 if !should_close.await? {
5906 return Ok(());
5907 }
5908 }
5909 }
5910
5911 cx.update(|cx| cx.restart(binary_path))
5912 })
5913 .detach_and_log_err(cx);
5914}
5915
5916fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5917 let mut parts = value.split(',');
5918 let x: usize = parts.next()?.parse().ok()?;
5919 let y: usize = parts.next()?.parse().ok()?;
5920 Some(point(px(x as f32), px(y as f32)))
5921}
5922
5923fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5924 let mut parts = value.split(',');
5925 let width: usize = parts.next()?.parse().ok()?;
5926 let height: usize = parts.next()?.parse().ok()?;
5927 Some(size(px(width as f32), px(height as f32)))
5928}
5929
5930pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5931 const BORDER_SIZE: Pixels = px(1.0);
5932 let decorations = cx.window_decorations();
5933
5934 if matches!(decorations, Decorations::Client { .. }) {
5935 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5936 }
5937
5938 struct GlobalResizeEdge(ResizeEdge);
5939 impl Global for GlobalResizeEdge {}
5940
5941 div()
5942 .id("window-backdrop")
5943 .bg(transparent_black())
5944 .map(|div| match decorations {
5945 Decorations::Server => div,
5946 Decorations::Client { tiling, .. } => div
5947 .when(!(tiling.top || tiling.right), |div| {
5948 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5949 })
5950 .when(!(tiling.top || tiling.left), |div| {
5951 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5952 })
5953 .when(!(tiling.bottom || tiling.right), |div| {
5954 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5955 })
5956 .when(!(tiling.bottom || tiling.left), |div| {
5957 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5958 })
5959 .when(!tiling.top, |div| {
5960 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5961 })
5962 .when(!tiling.bottom, |div| {
5963 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5964 })
5965 .when(!tiling.left, |div| {
5966 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5967 })
5968 .when(!tiling.right, |div| {
5969 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5970 })
5971 .on_mouse_move(move |e, cx| {
5972 let size = cx.window_bounds().get_bounds().size;
5973 let pos = e.position;
5974
5975 let new_edge =
5976 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5977
5978 let edge = cx.try_global::<GlobalResizeEdge>();
5979 if new_edge != edge.map(|edge| edge.0) {
5980 cx.window_handle()
5981 .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
5982 .ok();
5983 }
5984 })
5985 .on_mouse_down(MouseButton::Left, move |e, cx| {
5986 let size = cx.window_bounds().get_bounds().size;
5987 let pos = e.position;
5988
5989 let edge = match resize_edge(
5990 pos,
5991 theme::CLIENT_SIDE_DECORATION_SHADOW,
5992 size,
5993 tiling,
5994 ) {
5995 Some(value) => value,
5996 None => return,
5997 };
5998
5999 cx.start_window_resize(edge);
6000 }),
6001 })
6002 .size_full()
6003 .child(
6004 div()
6005 .cursor(CursorStyle::Arrow)
6006 .map(|div| match decorations {
6007 Decorations::Server => div,
6008 Decorations::Client { tiling } => div
6009 .border_color(cx.theme().colors().border)
6010 .when(!(tiling.top || tiling.right), |div| {
6011 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6012 })
6013 .when(!(tiling.top || tiling.left), |div| {
6014 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6015 })
6016 .when(!(tiling.bottom || tiling.right), |div| {
6017 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6018 })
6019 .when(!(tiling.bottom || tiling.left), |div| {
6020 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6021 })
6022 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6023 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6024 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6025 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6026 .when(!tiling.is_tiled(), |div| {
6027 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6028 color: Hsla {
6029 h: 0.,
6030 s: 0.,
6031 l: 0.,
6032 a: 0.4,
6033 },
6034 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6035 spread_radius: px(0.),
6036 offset: point(px(0.0), px(0.0)),
6037 }])
6038 }),
6039 })
6040 .on_mouse_move(|_e, cx| {
6041 cx.stop_propagation();
6042 })
6043 .size_full()
6044 .child(element),
6045 )
6046 .map(|div| match decorations {
6047 Decorations::Server => div,
6048 Decorations::Client { tiling, .. } => div.child(
6049 canvas(
6050 |_bounds, cx| {
6051 cx.insert_hitbox(
6052 Bounds::new(
6053 point(px(0.0), px(0.0)),
6054 cx.window_bounds().get_bounds().size,
6055 ),
6056 false,
6057 )
6058 },
6059 move |_bounds, hitbox, cx| {
6060 let mouse = cx.mouse_position();
6061 let size = cx.window_bounds().get_bounds().size;
6062 let Some(edge) =
6063 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6064 else {
6065 return;
6066 };
6067 cx.set_global(GlobalResizeEdge(edge));
6068 cx.set_cursor_style(
6069 match edge {
6070 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6071 ResizeEdge::Left | ResizeEdge::Right => {
6072 CursorStyle::ResizeLeftRight
6073 }
6074 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6075 CursorStyle::ResizeUpLeftDownRight
6076 }
6077 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6078 CursorStyle::ResizeUpRightDownLeft
6079 }
6080 },
6081 &hitbox,
6082 );
6083 },
6084 )
6085 .size_full()
6086 .absolute(),
6087 ),
6088 })
6089}
6090
6091fn resize_edge(
6092 pos: Point<Pixels>,
6093 shadow_size: Pixels,
6094 window_size: Size<Pixels>,
6095 tiling: Tiling,
6096) -> Option<ResizeEdge> {
6097 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6098 if bounds.contains(&pos) {
6099 return None;
6100 }
6101
6102 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6103 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6104 if !tiling.top && top_left_bounds.contains(&pos) {
6105 return Some(ResizeEdge::TopLeft);
6106 }
6107
6108 let top_right_bounds = Bounds::new(
6109 Point::new(window_size.width - corner_size.width, px(0.)),
6110 corner_size,
6111 );
6112 if !tiling.top && top_right_bounds.contains(&pos) {
6113 return Some(ResizeEdge::TopRight);
6114 }
6115
6116 let bottom_left_bounds = Bounds::new(
6117 Point::new(px(0.), window_size.height - corner_size.height),
6118 corner_size,
6119 );
6120 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6121 return Some(ResizeEdge::BottomLeft);
6122 }
6123
6124 let bottom_right_bounds = Bounds::new(
6125 Point::new(
6126 window_size.width - corner_size.width,
6127 window_size.height - corner_size.height,
6128 ),
6129 corner_size,
6130 );
6131 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6132 return Some(ResizeEdge::BottomRight);
6133 }
6134
6135 if !tiling.top && pos.y < shadow_size {
6136 Some(ResizeEdge::Top)
6137 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6138 Some(ResizeEdge::Bottom)
6139 } else if !tiling.left && pos.x < shadow_size {
6140 Some(ResizeEdge::Left)
6141 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6142 Some(ResizeEdge::Right)
6143 } else {
6144 None
6145 }
6146}
6147
6148fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext) {
6149 if pane == active_pane {
6150 return;
6151 } else if pane.read(cx).items_len() == 0 {
6152 pane.update(cx, |_, cx| {
6153 cx.emit(pane::Event::Remove {
6154 focus_on_pane: None,
6155 });
6156 })
6157 } else {
6158 move_all_items(pane, active_pane, cx);
6159 }
6160}
6161
6162fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext) {
6163 let destination_is_different = from_pane != to_pane;
6164 let mut moved_items = 0;
6165 for (item_ix, item_handle) in from_pane
6166 .read(cx)
6167 .items()
6168 .enumerate()
6169 .map(|(ix, item)| (ix, item.clone()))
6170 .collect::<Vec<_>>()
6171 {
6172 let ix = item_ix - moved_items;
6173 if destination_is_different {
6174 // Close item from previous pane
6175 from_pane.update(cx, |source, cx| {
6176 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6177 });
6178 moved_items += 1;
6179 }
6180
6181 // This automatically removes duplicate items in the pane
6182 to_pane.update(cx, |destination, cx| {
6183 destination.add_item(item_handle, true, true, None, cx);
6184 destination.focus(cx)
6185 });
6186 }
6187}
6188
6189pub fn move_item(
6190 source: &View<Pane>,
6191 destination: &View<Pane>,
6192 item_id_to_move: EntityId,
6193 destination_index: usize,
6194 cx: &mut WindowContext,
6195) {
6196 let Some((item_ix, item_handle)) = source
6197 .read(cx)
6198 .items()
6199 .enumerate()
6200 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6201 .map(|(ix, item)| (ix, item.clone()))
6202 else {
6203 // Tab was closed during drag
6204 return;
6205 };
6206
6207 if source != destination {
6208 // Close item from previous pane
6209 source.update(cx, |source, cx| {
6210 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6211 });
6212 }
6213
6214 // This automatically removes duplicate items in the pane
6215 destination.update(cx, |destination, cx| {
6216 destination.add_item(item_handle, true, true, Some(destination_index), cx);
6217 destination.focus(cx)
6218 });
6219}
6220
6221pub fn move_active_item(
6222 source: &View<Pane>,
6223 destination: &View<Pane>,
6224 focus_destination: bool,
6225 close_if_empty: bool,
6226 cx: &mut WindowContext,
6227) {
6228 if source == destination {
6229 return;
6230 }
6231 let Some(active_item) = source.read(cx).active_item() else {
6232 return;
6233 };
6234 source.update(cx, |source_pane, cx| {
6235 let item_id = active_item.item_id();
6236 source_pane.remove_item(item_id, false, close_if_empty, cx);
6237 destination.update(cx, |target_pane, cx| {
6238 target_pane.add_item(
6239 active_item,
6240 focus_destination,
6241 focus_destination,
6242 Some(target_pane.items_len()),
6243 cx,
6244 );
6245 });
6246 });
6247}
6248
6249#[cfg(test)]
6250mod tests {
6251 use std::{cell::RefCell, rc::Rc};
6252
6253 use super::*;
6254 use crate::{
6255 dock::{test::TestPanel, PanelEvent},
6256 item::{
6257 test::{TestItem, TestProjectItem},
6258 ItemEvent,
6259 },
6260 };
6261 use fs::FakeFs;
6262 use gpui::{
6263 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6264 UpdateGlobal, VisualTestContext,
6265 };
6266 use project::{Project, ProjectEntryId};
6267 use serde_json::json;
6268 use settings::SettingsStore;
6269
6270 #[gpui::test]
6271 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6272 init_test(cx);
6273
6274 let fs = FakeFs::new(cx.executor());
6275 let project = Project::test(fs, [], cx).await;
6276 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6277
6278 // Adding an item with no ambiguity renders the tab without detail.
6279 let item1 = cx.new_view(|cx| {
6280 let mut item = TestItem::new(cx);
6281 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6282 item
6283 });
6284 workspace.update(cx, |workspace, cx| {
6285 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6286 });
6287 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6288
6289 // Adding an item that creates ambiguity increases the level of detail on
6290 // both tabs.
6291 let item2 = cx.new_view(|cx| {
6292 let mut item = TestItem::new(cx);
6293 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6294 item
6295 });
6296 workspace.update(cx, |workspace, cx| {
6297 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6298 });
6299 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6300 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6301
6302 // Adding an item that creates ambiguity increases the level of detail only
6303 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6304 // we stop at the highest detail available.
6305 let item3 = cx.new_view(|cx| {
6306 let mut item = TestItem::new(cx);
6307 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6308 item
6309 });
6310 workspace.update(cx, |workspace, cx| {
6311 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6312 });
6313 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6314 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6315 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6316 }
6317
6318 #[gpui::test]
6319 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6320 init_test(cx);
6321
6322 let fs = FakeFs::new(cx.executor());
6323 fs.insert_tree(
6324 "/root1",
6325 json!({
6326 "one.txt": "",
6327 "two.txt": "",
6328 }),
6329 )
6330 .await;
6331 fs.insert_tree(
6332 "/root2",
6333 json!({
6334 "three.txt": "",
6335 }),
6336 )
6337 .await;
6338
6339 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6340 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6341 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6342 let worktree_id = project.update(cx, |project, cx| {
6343 project.worktrees(cx).next().unwrap().read(cx).id()
6344 });
6345
6346 let item1 = cx.new_view(|cx| {
6347 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6348 });
6349 let item2 = cx.new_view(|cx| {
6350 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6351 });
6352
6353 // Add an item to an empty pane
6354 workspace.update(cx, |workspace, cx| {
6355 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6356 });
6357 project.update(cx, |project, cx| {
6358 assert_eq!(
6359 project.active_entry(),
6360 project
6361 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6362 .map(|e| e.id)
6363 );
6364 });
6365 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6366
6367 // Add a second item to a non-empty pane
6368 workspace.update(cx, |workspace, cx| {
6369 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6370 });
6371 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6372 project.update(cx, |project, cx| {
6373 assert_eq!(
6374 project.active_entry(),
6375 project
6376 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6377 .map(|e| e.id)
6378 );
6379 });
6380
6381 // Close the active item
6382 pane.update(cx, |pane, cx| {
6383 pane.close_active_item(&Default::default(), cx).unwrap()
6384 })
6385 .await
6386 .unwrap();
6387 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6388 project.update(cx, |project, cx| {
6389 assert_eq!(
6390 project.active_entry(),
6391 project
6392 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6393 .map(|e| e.id)
6394 );
6395 });
6396
6397 // Add a project folder
6398 project
6399 .update(cx, |project, cx| {
6400 project.find_or_create_worktree("root2", true, cx)
6401 })
6402 .await
6403 .unwrap();
6404 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6405
6406 // Remove a project folder
6407 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6408 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6409 }
6410
6411 #[gpui::test]
6412 async fn test_close_window(cx: &mut TestAppContext) {
6413 init_test(cx);
6414
6415 let fs = FakeFs::new(cx.executor());
6416 fs.insert_tree("/root", json!({ "one": "" })).await;
6417
6418 let project = Project::test(fs, ["root".as_ref()], cx).await;
6419 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6420
6421 // When there are no dirty items, there's nothing to do.
6422 let item1 = cx.new_view(TestItem::new);
6423 workspace.update(cx, |w, cx| {
6424 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6425 });
6426 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6427 assert!(task.await.unwrap());
6428
6429 // When there are dirty untitled items, prompt to save each one. If the user
6430 // cancels any prompt, then abort.
6431 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6432 let item3 = cx.new_view(|cx| {
6433 TestItem::new(cx)
6434 .with_dirty(true)
6435 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6436 });
6437 workspace.update(cx, |w, cx| {
6438 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6439 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6440 });
6441 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6442 cx.executor().run_until_parked();
6443 cx.simulate_prompt_answer(2); // cancel save all
6444 cx.executor().run_until_parked();
6445 cx.simulate_prompt_answer(2); // cancel save all
6446 cx.executor().run_until_parked();
6447 assert!(!cx.has_pending_prompt());
6448 assert!(!task.await.unwrap());
6449 }
6450
6451 #[gpui::test]
6452 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6453 init_test(cx);
6454
6455 // Register TestItem as a serializable item
6456 cx.update(|cx| {
6457 register_serializable_item::<TestItem>(cx);
6458 });
6459
6460 let fs = FakeFs::new(cx.executor());
6461 fs.insert_tree("/root", json!({ "one": "" })).await;
6462
6463 let project = Project::test(fs, ["root".as_ref()], cx).await;
6464 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6465
6466 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6467 let item1 = cx.new_view(|cx| {
6468 TestItem::new(cx)
6469 .with_dirty(true)
6470 .with_serialize(|| Some(Task::ready(Ok(()))))
6471 });
6472 let item2 = cx.new_view(|cx| {
6473 TestItem::new(cx)
6474 .with_dirty(true)
6475 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6476 .with_serialize(|| Some(Task::ready(Ok(()))))
6477 });
6478 workspace.update(cx, |w, cx| {
6479 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6480 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6481 });
6482 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6483 assert!(task.await.unwrap());
6484 }
6485
6486 #[gpui::test]
6487 async fn test_close_pane_items(cx: &mut TestAppContext) {
6488 init_test(cx);
6489
6490 let fs = FakeFs::new(cx.executor());
6491
6492 let project = Project::test(fs, None, cx).await;
6493 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6494
6495 let item1 = cx.new_view(|cx| {
6496 TestItem::new(cx)
6497 .with_dirty(true)
6498 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6499 });
6500 let item2 = cx.new_view(|cx| {
6501 TestItem::new(cx)
6502 .with_dirty(true)
6503 .with_conflict(true)
6504 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6505 });
6506 let item3 = cx.new_view(|cx| {
6507 TestItem::new(cx)
6508 .with_dirty(true)
6509 .with_conflict(true)
6510 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6511 });
6512 let item4 = cx.new_view(|cx| {
6513 TestItem::new(cx).with_dirty(true).with_project_items(&[{
6514 let project_item = TestProjectItem::new_untitled(cx);
6515 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6516 project_item
6517 }])
6518 });
6519 let pane = workspace.update(cx, |workspace, cx| {
6520 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6521 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6522 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6523 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6524 workspace.active_pane().clone()
6525 });
6526
6527 let close_items = pane.update(cx, |pane, cx| {
6528 pane.activate_item(1, true, true, cx);
6529 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6530 let item1_id = item1.item_id();
6531 let item3_id = item3.item_id();
6532 let item4_id = item4.item_id();
6533 pane.close_items(cx, SaveIntent::Close, move |id| {
6534 [item1_id, item3_id, item4_id].contains(&id)
6535 })
6536 });
6537 cx.executor().run_until_parked();
6538
6539 assert!(cx.has_pending_prompt());
6540 // Ignore "Save all" prompt
6541 cx.simulate_prompt_answer(2);
6542 cx.executor().run_until_parked();
6543 // There's a prompt to save item 1.
6544 pane.update(cx, |pane, _| {
6545 assert_eq!(pane.items_len(), 4);
6546 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6547 });
6548 // Confirm saving item 1.
6549 cx.simulate_prompt_answer(0);
6550 cx.executor().run_until_parked();
6551
6552 // Item 1 is saved. There's a prompt to save item 3.
6553 pane.update(cx, |pane, cx| {
6554 assert_eq!(item1.read(cx).save_count, 1);
6555 assert_eq!(item1.read(cx).save_as_count, 0);
6556 assert_eq!(item1.read(cx).reload_count, 0);
6557 assert_eq!(pane.items_len(), 3);
6558 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6559 });
6560 assert!(cx.has_pending_prompt());
6561
6562 // Cancel saving item 3.
6563 cx.simulate_prompt_answer(1);
6564 cx.executor().run_until_parked();
6565
6566 // Item 3 is reloaded. There's a prompt to save item 4.
6567 pane.update(cx, |pane, cx| {
6568 assert_eq!(item3.read(cx).save_count, 0);
6569 assert_eq!(item3.read(cx).save_as_count, 0);
6570 assert_eq!(item3.read(cx).reload_count, 1);
6571 assert_eq!(pane.items_len(), 2);
6572 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6573 });
6574 assert!(cx.has_pending_prompt());
6575
6576 // Confirm saving item 4.
6577 cx.simulate_prompt_answer(0);
6578 cx.executor().run_until_parked();
6579
6580 // There's a prompt for a path for item 4.
6581 cx.simulate_new_path_selection(|_| Some(Default::default()));
6582 close_items.await.unwrap();
6583
6584 // The requested items are closed.
6585 pane.update(cx, |pane, cx| {
6586 assert_eq!(item4.read(cx).save_count, 0);
6587 assert_eq!(item4.read(cx).save_as_count, 1);
6588 assert_eq!(item4.read(cx).reload_count, 0);
6589 assert_eq!(pane.items_len(), 1);
6590 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6591 });
6592 }
6593
6594 #[gpui::test]
6595 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6596 init_test(cx);
6597
6598 let fs = FakeFs::new(cx.executor());
6599 let project = Project::test(fs, [], cx).await;
6600 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6601
6602 // Create several workspace items with single project entries, and two
6603 // workspace items with multiple project entries.
6604 let single_entry_items = (0..=4)
6605 .map(|project_entry_id| {
6606 cx.new_view(|cx| {
6607 TestItem::new(cx)
6608 .with_dirty(true)
6609 .with_project_items(&[dirty_project_item(
6610 project_entry_id,
6611 &format!("{project_entry_id}.txt"),
6612 cx,
6613 )])
6614 })
6615 })
6616 .collect::<Vec<_>>();
6617 let item_2_3 = cx.new_view(|cx| {
6618 TestItem::new(cx)
6619 .with_dirty(true)
6620 .with_singleton(false)
6621 .with_project_items(&[
6622 single_entry_items[2].read(cx).project_items[0].clone(),
6623 single_entry_items[3].read(cx).project_items[0].clone(),
6624 ])
6625 });
6626 let item_3_4 = cx.new_view(|cx| {
6627 TestItem::new(cx)
6628 .with_dirty(true)
6629 .with_singleton(false)
6630 .with_project_items(&[
6631 single_entry_items[3].read(cx).project_items[0].clone(),
6632 single_entry_items[4].read(cx).project_items[0].clone(),
6633 ])
6634 });
6635
6636 // Create two panes that contain the following project entries:
6637 // left pane:
6638 // multi-entry items: (2, 3)
6639 // single-entry items: 0, 1, 2, 3, 4
6640 // right pane:
6641 // single-entry items: 1
6642 // multi-entry items: (3, 4)
6643 let left_pane = workspace.update(cx, |workspace, cx| {
6644 let left_pane = workspace.active_pane().clone();
6645 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6646 for item in single_entry_items {
6647 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6648 }
6649 left_pane.update(cx, |pane, cx| {
6650 pane.activate_item(2, true, true, cx);
6651 });
6652
6653 let right_pane = workspace
6654 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6655 .unwrap();
6656
6657 right_pane.update(cx, |pane, cx| {
6658 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6659 });
6660
6661 left_pane
6662 });
6663
6664 cx.focus_view(&left_pane);
6665
6666 // When closing all of the items in the left pane, we should be prompted twice:
6667 // once for project entry 0, and once for project entry 2. Project entries 1,
6668 // 3, and 4 are all still open in the other paten. After those two
6669 // prompts, the task should complete.
6670
6671 let close = left_pane.update(cx, |pane, cx| {
6672 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6673 });
6674 cx.executor().run_until_parked();
6675
6676 // Discard "Save all" prompt
6677 cx.simulate_prompt_answer(2);
6678
6679 cx.executor().run_until_parked();
6680 left_pane.update(cx, |pane, cx| {
6681 assert_eq!(
6682 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6683 &[ProjectEntryId::from_proto(0)]
6684 );
6685 });
6686 cx.simulate_prompt_answer(0);
6687
6688 cx.executor().run_until_parked();
6689 left_pane.update(cx, |pane, cx| {
6690 assert_eq!(
6691 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6692 &[ProjectEntryId::from_proto(2)]
6693 );
6694 });
6695 cx.simulate_prompt_answer(0);
6696
6697 cx.executor().run_until_parked();
6698 close.await.unwrap();
6699 left_pane.update(cx, |pane, _| {
6700 assert_eq!(pane.items_len(), 0);
6701 });
6702 }
6703
6704 #[gpui::test]
6705 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6706 init_test(cx);
6707
6708 let fs = FakeFs::new(cx.executor());
6709 let project = Project::test(fs, [], cx).await;
6710 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6711 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6712
6713 let item = cx.new_view(|cx| {
6714 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6715 });
6716 let item_id = item.entity_id();
6717 workspace.update(cx, |workspace, cx| {
6718 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6719 });
6720
6721 // Autosave on window change.
6722 item.update(cx, |item, cx| {
6723 SettingsStore::update_global(cx, |settings, cx| {
6724 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6725 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6726 })
6727 });
6728 item.is_dirty = true;
6729 });
6730
6731 // Deactivating the window saves the file.
6732 cx.deactivate_window();
6733 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6734
6735 // Re-activating the window doesn't save the file.
6736 cx.update(|cx| cx.activate_window());
6737 cx.executor().run_until_parked();
6738 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6739
6740 // Autosave on focus change.
6741 item.update(cx, |item, cx| {
6742 cx.focus_self();
6743 SettingsStore::update_global(cx, |settings, cx| {
6744 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6745 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6746 })
6747 });
6748 item.is_dirty = true;
6749 });
6750
6751 // Blurring the item saves the file.
6752 item.update(cx, |_, cx| cx.blur());
6753 cx.executor().run_until_parked();
6754 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6755
6756 // Deactivating the window still saves the file.
6757 item.update(cx, |item, cx| {
6758 cx.focus_self();
6759 item.is_dirty = true;
6760 });
6761 cx.deactivate_window();
6762 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6763
6764 // Autosave after delay.
6765 item.update(cx, |item, cx| {
6766 SettingsStore::update_global(cx, |settings, cx| {
6767 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6768 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6769 })
6770 });
6771 item.is_dirty = true;
6772 cx.emit(ItemEvent::Edit);
6773 });
6774
6775 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6776 cx.executor().advance_clock(Duration::from_millis(250));
6777 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6778
6779 // After delay expires, the file is saved.
6780 cx.executor().advance_clock(Duration::from_millis(250));
6781 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6782
6783 // Autosave on focus change, ensuring closing the tab counts as such.
6784 item.update(cx, |item, cx| {
6785 SettingsStore::update_global(cx, |settings, cx| {
6786 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6787 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6788 })
6789 });
6790 item.is_dirty = true;
6791 for project_item in &mut item.project_items {
6792 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6793 }
6794 });
6795
6796 pane.update(cx, |pane, cx| {
6797 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6798 })
6799 .await
6800 .unwrap();
6801 assert!(!cx.has_pending_prompt());
6802 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6803
6804 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6805 workspace.update(cx, |workspace, cx| {
6806 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6807 });
6808 item.update(cx, |item, cx| {
6809 item.project_items[0].update(cx, |item, _| {
6810 item.entry_id = None;
6811 });
6812 item.is_dirty = true;
6813 cx.blur();
6814 });
6815 cx.run_until_parked();
6816 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6817
6818 // Ensure autosave is prevented for deleted files also when closing the buffer.
6819 let _close_items = pane.update(cx, |pane, cx| {
6820 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6821 });
6822 cx.run_until_parked();
6823 assert!(cx.has_pending_prompt());
6824 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6825 }
6826
6827 #[gpui::test]
6828 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6829 init_test(cx);
6830
6831 let fs = FakeFs::new(cx.executor());
6832
6833 let project = Project::test(fs, [], cx).await;
6834 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6835
6836 let item = cx.new_view(|cx| {
6837 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6838 });
6839 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6840 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6841 let toolbar_notify_count = Rc::new(RefCell::new(0));
6842
6843 workspace.update(cx, |workspace, cx| {
6844 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6845 let toolbar_notification_count = toolbar_notify_count.clone();
6846 cx.observe(&toolbar, move |_, _, _| {
6847 *toolbar_notification_count.borrow_mut() += 1
6848 })
6849 .detach();
6850 });
6851
6852 pane.update(cx, |pane, _| {
6853 assert!(!pane.can_navigate_backward());
6854 assert!(!pane.can_navigate_forward());
6855 });
6856
6857 item.update(cx, |item, cx| {
6858 item.set_state("one".to_string(), cx);
6859 });
6860
6861 // Toolbar must be notified to re-render the navigation buttons
6862 assert_eq!(*toolbar_notify_count.borrow(), 1);
6863
6864 pane.update(cx, |pane, _| {
6865 assert!(pane.can_navigate_backward());
6866 assert!(!pane.can_navigate_forward());
6867 });
6868
6869 workspace
6870 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6871 .await
6872 .unwrap();
6873
6874 assert_eq!(*toolbar_notify_count.borrow(), 2);
6875 pane.update(cx, |pane, _| {
6876 assert!(!pane.can_navigate_backward());
6877 assert!(pane.can_navigate_forward());
6878 });
6879 }
6880
6881 #[gpui::test]
6882 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6883 init_test(cx);
6884 let fs = FakeFs::new(cx.executor());
6885
6886 let project = Project::test(fs, [], cx).await;
6887 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6888
6889 let panel = workspace.update(cx, |workspace, cx| {
6890 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6891 workspace.add_panel(panel.clone(), cx);
6892
6893 workspace
6894 .right_dock()
6895 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6896
6897 panel
6898 });
6899
6900 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6901 pane.update(cx, |pane, cx| {
6902 let item = cx.new_view(TestItem::new);
6903 pane.add_item(Box::new(item), true, true, None, cx);
6904 });
6905
6906 // Transfer focus from center to panel
6907 workspace.update(cx, |workspace, cx| {
6908 workspace.toggle_panel_focus::<TestPanel>(cx);
6909 });
6910
6911 workspace.update(cx, |workspace, cx| {
6912 assert!(workspace.right_dock().read(cx).is_open());
6913 assert!(!panel.is_zoomed(cx));
6914 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6915 });
6916
6917 // Transfer focus from panel to center
6918 workspace.update(cx, |workspace, cx| {
6919 workspace.toggle_panel_focus::<TestPanel>(cx);
6920 });
6921
6922 workspace.update(cx, |workspace, cx| {
6923 assert!(workspace.right_dock().read(cx).is_open());
6924 assert!(!panel.is_zoomed(cx));
6925 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6926 });
6927
6928 // Close the dock
6929 workspace.update(cx, |workspace, cx| {
6930 workspace.toggle_dock(DockPosition::Right, cx);
6931 });
6932
6933 workspace.update(cx, |workspace, cx| {
6934 assert!(!workspace.right_dock().read(cx).is_open());
6935 assert!(!panel.is_zoomed(cx));
6936 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6937 });
6938
6939 // Open the dock
6940 workspace.update(cx, |workspace, cx| {
6941 workspace.toggle_dock(DockPosition::Right, cx);
6942 });
6943
6944 workspace.update(cx, |workspace, cx| {
6945 assert!(workspace.right_dock().read(cx).is_open());
6946 assert!(!panel.is_zoomed(cx));
6947 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6948 });
6949
6950 // Focus and zoom panel
6951 panel.update(cx, |panel, cx| {
6952 cx.focus_self();
6953 panel.set_zoomed(true, cx)
6954 });
6955
6956 workspace.update(cx, |workspace, cx| {
6957 assert!(workspace.right_dock().read(cx).is_open());
6958 assert!(panel.is_zoomed(cx));
6959 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6960 });
6961
6962 // Transfer focus to the center closes the dock
6963 workspace.update(cx, |workspace, cx| {
6964 workspace.toggle_panel_focus::<TestPanel>(cx);
6965 });
6966
6967 workspace.update(cx, |workspace, cx| {
6968 assert!(!workspace.right_dock().read(cx).is_open());
6969 assert!(panel.is_zoomed(cx));
6970 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6971 });
6972
6973 // Transferring focus back to the panel keeps it zoomed
6974 workspace.update(cx, |workspace, cx| {
6975 workspace.toggle_panel_focus::<TestPanel>(cx);
6976 });
6977
6978 workspace.update(cx, |workspace, cx| {
6979 assert!(workspace.right_dock().read(cx).is_open());
6980 assert!(panel.is_zoomed(cx));
6981 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6982 });
6983
6984 // Close the dock while it is zoomed
6985 workspace.update(cx, |workspace, cx| {
6986 workspace.toggle_dock(DockPosition::Right, cx)
6987 });
6988
6989 workspace.update(cx, |workspace, cx| {
6990 assert!(!workspace.right_dock().read(cx).is_open());
6991 assert!(panel.is_zoomed(cx));
6992 assert!(workspace.zoomed.is_none());
6993 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6994 });
6995
6996 // Opening the dock, when it's zoomed, retains focus
6997 workspace.update(cx, |workspace, cx| {
6998 workspace.toggle_dock(DockPosition::Right, cx)
6999 });
7000
7001 workspace.update(cx, |workspace, cx| {
7002 assert!(workspace.right_dock().read(cx).is_open());
7003 assert!(panel.is_zoomed(cx));
7004 assert!(workspace.zoomed.is_some());
7005 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7006 });
7007
7008 // Unzoom and close the panel, zoom the active pane.
7009 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
7010 workspace.update(cx, |workspace, cx| {
7011 workspace.toggle_dock(DockPosition::Right, cx)
7012 });
7013 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
7014
7015 // Opening a dock unzooms the pane.
7016 workspace.update(cx, |workspace, cx| {
7017 workspace.toggle_dock(DockPosition::Right, cx)
7018 });
7019 workspace.update(cx, |workspace, cx| {
7020 let pane = pane.read(cx);
7021 assert!(!pane.is_zoomed());
7022 assert!(!pane.focus_handle(cx).is_focused(cx));
7023 assert!(workspace.right_dock().read(cx).is_open());
7024 assert!(workspace.zoomed.is_none());
7025 });
7026 }
7027
7028 #[gpui::test]
7029 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7030 init_test(cx);
7031
7032 let fs = FakeFs::new(cx.executor());
7033
7034 let project = Project::test(fs, None, cx).await;
7035 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7036
7037 // Let's arrange the panes like this:
7038 //
7039 // +-----------------------+
7040 // | top |
7041 // +------+--------+-------+
7042 // | left | center | right |
7043 // +------+--------+-------+
7044 // | bottom |
7045 // +-----------------------+
7046
7047 let top_item = cx.new_view(|cx| {
7048 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7049 });
7050 let bottom_item = cx.new_view(|cx| {
7051 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7052 });
7053 let left_item = cx.new_view(|cx| {
7054 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7055 });
7056 let right_item = cx.new_view(|cx| {
7057 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7058 });
7059 let center_item = cx.new_view(|cx| {
7060 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7061 });
7062
7063 let top_pane_id = workspace.update(cx, |workspace, cx| {
7064 let top_pane_id = workspace.active_pane().entity_id();
7065 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
7066 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
7067 top_pane_id
7068 });
7069 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
7070 let bottom_pane_id = workspace.active_pane().entity_id();
7071 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
7072 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
7073 bottom_pane_id
7074 });
7075 let left_pane_id = workspace.update(cx, |workspace, cx| {
7076 let left_pane_id = workspace.active_pane().entity_id();
7077 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
7078 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7079 left_pane_id
7080 });
7081 let right_pane_id = workspace.update(cx, |workspace, cx| {
7082 let right_pane_id = workspace.active_pane().entity_id();
7083 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
7084 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
7085 right_pane_id
7086 });
7087 let center_pane_id = workspace.update(cx, |workspace, cx| {
7088 let center_pane_id = workspace.active_pane().entity_id();
7089 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
7090 center_pane_id
7091 });
7092 cx.executor().run_until_parked();
7093
7094 workspace.update(cx, |workspace, cx| {
7095 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7096
7097 // Join into next from center pane into right
7098 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7099 });
7100
7101 workspace.update(cx, |workspace, cx| {
7102 let active_pane = workspace.active_pane();
7103 assert_eq!(right_pane_id, active_pane.entity_id());
7104 assert_eq!(2, active_pane.read(cx).items_len());
7105 let item_ids_in_pane =
7106 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7107 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7108 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7109
7110 // Join into next from right pane into bottom
7111 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7112 });
7113
7114 workspace.update(cx, |workspace, cx| {
7115 let active_pane = workspace.active_pane();
7116 assert_eq!(bottom_pane_id, active_pane.entity_id());
7117 assert_eq!(3, active_pane.read(cx).items_len());
7118 let item_ids_in_pane =
7119 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7120 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7121 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7122 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7123
7124 // Join into next from bottom pane into left
7125 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7126 });
7127
7128 workspace.update(cx, |workspace, cx| {
7129 let active_pane = workspace.active_pane();
7130 assert_eq!(left_pane_id, active_pane.entity_id());
7131 assert_eq!(4, active_pane.read(cx).items_len());
7132 let item_ids_in_pane =
7133 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7134 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7135 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7136 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7137 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7138
7139 // Join into next from left pane into top
7140 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7141 });
7142
7143 workspace.update(cx, |workspace, cx| {
7144 let active_pane = workspace.active_pane();
7145 assert_eq!(top_pane_id, active_pane.entity_id());
7146 assert_eq!(5, active_pane.read(cx).items_len());
7147 let item_ids_in_pane =
7148 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7149 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7150 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7151 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7152 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7153 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7154
7155 // Single pane left: no-op
7156 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7157 });
7158
7159 workspace.update(cx, |workspace, _cx| {
7160 let active_pane = workspace.active_pane();
7161 assert_eq!(top_pane_id, active_pane.entity_id());
7162 });
7163 }
7164
7165 fn add_an_item_to_active_pane(
7166 cx: &mut VisualTestContext,
7167 workspace: &View<Workspace>,
7168 item_id: u64,
7169 ) -> View<TestItem> {
7170 let item = cx.new_view(|cx| {
7171 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7172 item_id,
7173 "item{item_id}.txt",
7174 cx,
7175 )])
7176 });
7177 workspace.update(cx, |workspace, cx| {
7178 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7179 });
7180 return item;
7181 }
7182
7183 fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7184 return workspace.update(cx, |workspace, cx| {
7185 let new_pane =
7186 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7187 new_pane
7188 });
7189 }
7190
7191 #[gpui::test]
7192 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7193 init_test(cx);
7194 let fs = FakeFs::new(cx.executor());
7195 let project = Project::test(fs, None, cx).await;
7196 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7197
7198 add_an_item_to_active_pane(cx, &workspace, 1);
7199 split_pane(cx, &workspace);
7200 add_an_item_to_active_pane(cx, &workspace, 2);
7201 split_pane(cx, &workspace); // empty pane
7202 split_pane(cx, &workspace);
7203 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7204
7205 cx.executor().run_until_parked();
7206
7207 workspace.update(cx, |workspace, cx| {
7208 let num_panes = workspace.panes().len();
7209 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7210 let active_item = workspace
7211 .active_pane()
7212 .read(cx)
7213 .active_item()
7214 .expect("item is in focus");
7215
7216 assert_eq!(num_panes, 4);
7217 assert_eq!(num_items_in_current_pane, 1);
7218 assert_eq!(active_item.item_id(), last_item.item_id());
7219 });
7220
7221 workspace.update(cx, |workspace, cx| {
7222 workspace.join_all_panes(cx);
7223 });
7224
7225 workspace.update(cx, |workspace, cx| {
7226 let num_panes = workspace.panes().len();
7227 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7228 let active_item = workspace
7229 .active_pane()
7230 .read(cx)
7231 .active_item()
7232 .expect("item is in focus");
7233
7234 assert_eq!(num_panes, 1);
7235 assert_eq!(num_items_in_current_pane, 3);
7236 assert_eq!(active_item.item_id(), last_item.item_id());
7237 });
7238 }
7239 struct TestModal(FocusHandle);
7240
7241 impl TestModal {
7242 fn new(cx: &mut ViewContext<Self>) -> Self {
7243 Self(cx.focus_handle())
7244 }
7245 }
7246
7247 impl EventEmitter<DismissEvent> for TestModal {}
7248
7249 impl FocusableView for TestModal {
7250 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7251 self.0.clone()
7252 }
7253 }
7254
7255 impl ModalView for TestModal {}
7256
7257 impl Render for TestModal {
7258 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7259 div().track_focus(&self.0)
7260 }
7261 }
7262
7263 #[gpui::test]
7264 async fn test_panels(cx: &mut gpui::TestAppContext) {
7265 init_test(cx);
7266 let fs = FakeFs::new(cx.executor());
7267
7268 let project = Project::test(fs, [], cx).await;
7269 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7270
7271 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7272 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7273 workspace.add_panel(panel_1.clone(), cx);
7274 workspace.toggle_dock(DockPosition::Left, cx);
7275 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7276 workspace.add_panel(panel_2.clone(), cx);
7277 workspace.toggle_dock(DockPosition::Right, cx);
7278
7279 let left_dock = workspace.left_dock();
7280 assert_eq!(
7281 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7282 panel_1.panel_id()
7283 );
7284 assert_eq!(
7285 left_dock.read(cx).active_panel_size(cx).unwrap(),
7286 panel_1.size(cx)
7287 );
7288
7289 left_dock.update(cx, |left_dock, cx| {
7290 left_dock.resize_active_panel(Some(px(1337.)), cx)
7291 });
7292 assert_eq!(
7293 workspace
7294 .right_dock()
7295 .read(cx)
7296 .visible_panel()
7297 .unwrap()
7298 .panel_id(),
7299 panel_2.panel_id(),
7300 );
7301
7302 (panel_1, panel_2)
7303 });
7304
7305 // Move panel_1 to the right
7306 panel_1.update(cx, |panel_1, cx| {
7307 panel_1.set_position(DockPosition::Right, cx)
7308 });
7309
7310 workspace.update(cx, |workspace, cx| {
7311 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7312 // Since it was the only panel on the left, the left dock should now be closed.
7313 assert!(!workspace.left_dock().read(cx).is_open());
7314 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7315 let right_dock = workspace.right_dock();
7316 assert_eq!(
7317 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7318 panel_1.panel_id()
7319 );
7320 assert_eq!(
7321 right_dock.read(cx).active_panel_size(cx).unwrap(),
7322 px(1337.)
7323 );
7324
7325 // Now we move panel_2 to the left
7326 panel_2.set_position(DockPosition::Left, cx);
7327 });
7328
7329 workspace.update(cx, |workspace, cx| {
7330 // Since panel_2 was not visible on the right, we don't open the left dock.
7331 assert!(!workspace.left_dock().read(cx).is_open());
7332 // And the right dock is unaffected in its displaying of panel_1
7333 assert!(workspace.right_dock().read(cx).is_open());
7334 assert_eq!(
7335 workspace
7336 .right_dock()
7337 .read(cx)
7338 .visible_panel()
7339 .unwrap()
7340 .panel_id(),
7341 panel_1.panel_id(),
7342 );
7343 });
7344
7345 // Move panel_1 back to the left
7346 panel_1.update(cx, |panel_1, cx| {
7347 panel_1.set_position(DockPosition::Left, cx)
7348 });
7349
7350 workspace.update(cx, |workspace, cx| {
7351 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7352 let left_dock = workspace.left_dock();
7353 assert!(left_dock.read(cx).is_open());
7354 assert_eq!(
7355 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7356 panel_1.panel_id()
7357 );
7358 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7359 // And the right dock should be closed as it no longer has any panels.
7360 assert!(!workspace.right_dock().read(cx).is_open());
7361
7362 // Now we move panel_1 to the bottom
7363 panel_1.set_position(DockPosition::Bottom, cx);
7364 });
7365
7366 workspace.update(cx, |workspace, cx| {
7367 // Since panel_1 was visible on the left, we close the left dock.
7368 assert!(!workspace.left_dock().read(cx).is_open());
7369 // The bottom dock is sized based on the panel's default size,
7370 // since the panel orientation changed from vertical to horizontal.
7371 let bottom_dock = workspace.bottom_dock();
7372 assert_eq!(
7373 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7374 panel_1.size(cx),
7375 );
7376 // Close bottom dock and move panel_1 back to the left.
7377 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7378 panel_1.set_position(DockPosition::Left, cx);
7379 });
7380
7381 // Emit activated event on panel 1
7382 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7383
7384 // Now the left dock is open and panel_1 is active and focused.
7385 workspace.update(cx, |workspace, cx| {
7386 let left_dock = workspace.left_dock();
7387 assert!(left_dock.read(cx).is_open());
7388 assert_eq!(
7389 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7390 panel_1.panel_id(),
7391 );
7392 assert!(panel_1.focus_handle(cx).is_focused(cx));
7393 });
7394
7395 // Emit closed event on panel 2, which is not active
7396 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7397
7398 // Wo don't close the left dock, because panel_2 wasn't the active panel
7399 workspace.update(cx, |workspace, cx| {
7400 let left_dock = workspace.left_dock();
7401 assert!(left_dock.read(cx).is_open());
7402 assert_eq!(
7403 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7404 panel_1.panel_id(),
7405 );
7406 });
7407
7408 // Emitting a ZoomIn event shows the panel as zoomed.
7409 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7410 workspace.update(cx, |workspace, _| {
7411 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7412 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7413 });
7414
7415 // Move panel to another dock while it is zoomed
7416 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7417 workspace.update(cx, |workspace, _| {
7418 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7419
7420 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7421 });
7422
7423 // This is a helper for getting a:
7424 // - valid focus on an element,
7425 // - that isn't a part of the panes and panels system of the Workspace,
7426 // - and doesn't trigger the 'on_focus_lost' API.
7427 let focus_other_view = {
7428 let workspace = workspace.clone();
7429 move |cx: &mut VisualTestContext| {
7430 workspace.update(cx, |workspace, cx| {
7431 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7432 workspace.toggle_modal(cx, TestModal::new);
7433 workspace.toggle_modal(cx, TestModal::new);
7434 } else {
7435 workspace.toggle_modal(cx, TestModal::new);
7436 }
7437 })
7438 }
7439 };
7440
7441 // If focus is transferred to another view that's not a panel or another pane, we still show
7442 // the panel as zoomed.
7443 focus_other_view(cx);
7444 workspace.update(cx, |workspace, _| {
7445 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7446 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7447 });
7448
7449 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7450 workspace.update(cx, |_, cx| cx.focus_self());
7451 workspace.update(cx, |workspace, _| {
7452 assert_eq!(workspace.zoomed, None);
7453 assert_eq!(workspace.zoomed_position, None);
7454 });
7455
7456 // If focus is transferred again to another view that's not a panel or a pane, we won't
7457 // show the panel as zoomed because it wasn't zoomed before.
7458 focus_other_view(cx);
7459 workspace.update(cx, |workspace, _| {
7460 assert_eq!(workspace.zoomed, None);
7461 assert_eq!(workspace.zoomed_position, None);
7462 });
7463
7464 // When the panel is activated, it is zoomed again.
7465 cx.dispatch_action(ToggleRightDock);
7466 workspace.update(cx, |workspace, _| {
7467 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7468 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7469 });
7470
7471 // Emitting a ZoomOut event unzooms the panel.
7472 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7473 workspace.update(cx, |workspace, _| {
7474 assert_eq!(workspace.zoomed, None);
7475 assert_eq!(workspace.zoomed_position, None);
7476 });
7477
7478 // Emit closed event on panel 1, which is active
7479 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7480
7481 // Now the left dock is closed, because panel_1 was the active panel
7482 workspace.update(cx, |workspace, cx| {
7483 let right_dock = workspace.right_dock();
7484 assert!(!right_dock.read(cx).is_open());
7485 });
7486 }
7487
7488 #[gpui::test]
7489 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
7490 init_test(cx);
7491
7492 let fs = FakeFs::new(cx.background_executor.clone());
7493 let project = Project::test(fs, [], cx).await;
7494 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7495 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7496
7497 let dirty_regular_buffer = cx.new_view(|cx| {
7498 TestItem::new(cx)
7499 .with_dirty(true)
7500 .with_label("1.txt")
7501 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7502 });
7503 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7504 TestItem::new(cx)
7505 .with_dirty(true)
7506 .with_label("2.txt")
7507 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7508 });
7509 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7510 TestItem::new(cx)
7511 .with_dirty(true)
7512 .with_singleton(false)
7513 .with_label("Fake Project Search")
7514 .with_project_items(&[
7515 dirty_regular_buffer.read(cx).project_items[0].clone(),
7516 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7517 ])
7518 });
7519 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7520 workspace.update(cx, |workspace, cx| {
7521 workspace.add_item(
7522 pane.clone(),
7523 Box::new(dirty_regular_buffer.clone()),
7524 None,
7525 false,
7526 false,
7527 cx,
7528 );
7529 workspace.add_item(
7530 pane.clone(),
7531 Box::new(dirty_regular_buffer_2.clone()),
7532 None,
7533 false,
7534 false,
7535 cx,
7536 );
7537 workspace.add_item(
7538 pane.clone(),
7539 Box::new(dirty_multi_buffer_with_both.clone()),
7540 None,
7541 false,
7542 false,
7543 cx,
7544 );
7545 });
7546
7547 pane.update(cx, |pane, cx| {
7548 pane.activate_item(2, true, true, cx);
7549 assert_eq!(
7550 pane.active_item().unwrap().item_id(),
7551 multi_buffer_with_both_files_id,
7552 "Should select the multi buffer in the pane"
7553 );
7554 });
7555 let close_all_but_multi_buffer_task = pane
7556 .update(cx, |pane, cx| {
7557 pane.close_inactive_items(
7558 &CloseInactiveItems {
7559 save_intent: Some(SaveIntent::Save),
7560 close_pinned: true,
7561 },
7562 cx,
7563 )
7564 })
7565 .expect("should have inactive files to close");
7566 cx.background_executor.run_until_parked();
7567 assert!(
7568 !cx.has_pending_prompt(),
7569 "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
7570 );
7571 close_all_but_multi_buffer_task
7572 .await
7573 .expect("Closing all buffers but the multi buffer failed");
7574 pane.update(cx, |pane, cx| {
7575 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7576 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
7577 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7578 assert_eq!(pane.items_len(), 1);
7579 assert_eq!(
7580 pane.active_item().unwrap().item_id(),
7581 multi_buffer_with_both_files_id,
7582 "Should have only the multi buffer left in the pane"
7583 );
7584 assert!(
7585 dirty_multi_buffer_with_both.read(cx).is_dirty,
7586 "The multi buffer containing the unsaved buffer should still be dirty"
7587 );
7588 });
7589
7590 let close_multi_buffer_task = pane
7591 .update(cx, |pane, cx| {
7592 pane.close_active_item(
7593 &CloseActiveItem {
7594 save_intent: Some(SaveIntent::Close),
7595 },
7596 cx,
7597 )
7598 })
7599 .expect("should have the multi buffer to close");
7600 cx.background_executor.run_until_parked();
7601 assert!(
7602 cx.has_pending_prompt(),
7603 "Dirty multi buffer should prompt a save dialog"
7604 );
7605 cx.simulate_prompt_answer(0);
7606 cx.background_executor.run_until_parked();
7607 close_multi_buffer_task
7608 .await
7609 .expect("Closing the multi buffer failed");
7610 pane.update(cx, |pane, cx| {
7611 assert_eq!(
7612 dirty_multi_buffer_with_both.read(cx).save_count,
7613 1,
7614 "Multi buffer item should get be saved"
7615 );
7616 // Test impl does not save inner items, so we do not assert them
7617 assert_eq!(
7618 pane.items_len(),
7619 0,
7620 "No more items should be left in the pane"
7621 );
7622 assert!(pane.active_item().is_none());
7623 });
7624 }
7625
7626 #[gpui::test]
7627 async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
7628 cx: &mut TestAppContext,
7629 ) {
7630 init_test(cx);
7631
7632 let fs = FakeFs::new(cx.background_executor.clone());
7633 let project = Project::test(fs, [], cx).await;
7634 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7635 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7636
7637 let dirty_regular_buffer = cx.new_view(|cx| {
7638 TestItem::new(cx)
7639 .with_dirty(true)
7640 .with_label("1.txt")
7641 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7642 });
7643 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7644 TestItem::new(cx)
7645 .with_dirty(true)
7646 .with_label("2.txt")
7647 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7648 });
7649 let clear_regular_buffer = cx.new_view(|cx| {
7650 TestItem::new(cx)
7651 .with_label("3.txt")
7652 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7653 });
7654
7655 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7656 TestItem::new(cx)
7657 .with_dirty(true)
7658 .with_singleton(false)
7659 .with_label("Fake Project Search")
7660 .with_project_items(&[
7661 dirty_regular_buffer.read(cx).project_items[0].clone(),
7662 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7663 clear_regular_buffer.read(cx).project_items[0].clone(),
7664 ])
7665 });
7666 workspace.update(cx, |workspace, cx| {
7667 workspace.add_item(
7668 pane.clone(),
7669 Box::new(dirty_regular_buffer.clone()),
7670 None,
7671 false,
7672 false,
7673 cx,
7674 );
7675 workspace.add_item(
7676 pane.clone(),
7677 Box::new(dirty_multi_buffer_with_both.clone()),
7678 None,
7679 false,
7680 false,
7681 cx,
7682 );
7683 });
7684
7685 pane.update(cx, |pane, cx| {
7686 pane.activate_item(0, true, true, cx);
7687 assert_eq!(
7688 pane.active_item().unwrap().item_id(),
7689 dirty_regular_buffer.item_id(),
7690 "Should select the dirty singleton buffer in the pane"
7691 );
7692 });
7693 let close_singleton_buffer_task = pane
7694 .update(cx, |pane, cx| {
7695 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7696 })
7697 .expect("should have active singleton buffer to close");
7698 cx.background_executor.run_until_parked();
7699 assert!(
7700 !cx.has_pending_prompt(),
7701 "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
7702 );
7703
7704 close_singleton_buffer_task
7705 .await
7706 .expect("Should not fail closing the singleton buffer");
7707 pane.update(cx, |pane, cx| {
7708 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7709 assert_eq!(
7710 dirty_multi_buffer_with_both.read(cx).save_count,
7711 0,
7712 "Multi buffer itself should not be saved"
7713 );
7714 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7715 assert_eq!(
7716 pane.items_len(),
7717 1,
7718 "A dirty multi buffer should be present in the pane"
7719 );
7720 assert_eq!(
7721 pane.active_item().unwrap().item_id(),
7722 dirty_multi_buffer_with_both.item_id(),
7723 "Should activate the only remaining item in the pane"
7724 );
7725 });
7726 }
7727
7728 #[gpui::test]
7729 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
7730 cx: &mut TestAppContext,
7731 ) {
7732 init_test(cx);
7733
7734 let fs = FakeFs::new(cx.background_executor.clone());
7735 let project = Project::test(fs, [], cx).await;
7736 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7737 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7738
7739 let dirty_regular_buffer = cx.new_view(|cx| {
7740 TestItem::new(cx)
7741 .with_dirty(true)
7742 .with_label("1.txt")
7743 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7744 });
7745 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7746 TestItem::new(cx)
7747 .with_dirty(true)
7748 .with_label("2.txt")
7749 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7750 });
7751 let clear_regular_buffer = cx.new_view(|cx| {
7752 TestItem::new(cx)
7753 .with_label("3.txt")
7754 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7755 });
7756
7757 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7758 TestItem::new(cx)
7759 .with_dirty(true)
7760 .with_singleton(false)
7761 .with_label("Fake Project Search")
7762 .with_project_items(&[
7763 dirty_regular_buffer.read(cx).project_items[0].clone(),
7764 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7765 clear_regular_buffer.read(cx).project_items[0].clone(),
7766 ])
7767 });
7768 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7769 workspace.update(cx, |workspace, cx| {
7770 workspace.add_item(
7771 pane.clone(),
7772 Box::new(dirty_regular_buffer.clone()),
7773 None,
7774 false,
7775 false,
7776 cx,
7777 );
7778 workspace.add_item(
7779 pane.clone(),
7780 Box::new(dirty_multi_buffer_with_both.clone()),
7781 None,
7782 false,
7783 false,
7784 cx,
7785 );
7786 });
7787
7788 pane.update(cx, |pane, cx| {
7789 pane.activate_item(1, true, true, cx);
7790 assert_eq!(
7791 pane.active_item().unwrap().item_id(),
7792 multi_buffer_with_both_files_id,
7793 "Should select the multi buffer in the pane"
7794 );
7795 });
7796 let _close_multi_buffer_task = pane
7797 .update(cx, |pane, cx| {
7798 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7799 })
7800 .expect("should have active multi buffer to close");
7801 cx.background_executor.run_until_parked();
7802 assert!(
7803 cx.has_pending_prompt(),
7804 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
7805 );
7806 }
7807
7808 #[gpui::test]
7809 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
7810 cx: &mut TestAppContext,
7811 ) {
7812 init_test(cx);
7813
7814 let fs = FakeFs::new(cx.background_executor.clone());
7815 let project = Project::test(fs, [], cx).await;
7816 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7817 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7818
7819 let dirty_regular_buffer = cx.new_view(|cx| {
7820 TestItem::new(cx)
7821 .with_dirty(true)
7822 .with_label("1.txt")
7823 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7824 });
7825 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7826 TestItem::new(cx)
7827 .with_dirty(true)
7828 .with_label("2.txt")
7829 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7830 });
7831 let clear_regular_buffer = cx.new_view(|cx| {
7832 TestItem::new(cx)
7833 .with_label("3.txt")
7834 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7835 });
7836
7837 let dirty_multi_buffer = cx.new_view(|cx| {
7838 TestItem::new(cx)
7839 .with_dirty(true)
7840 .with_singleton(false)
7841 .with_label("Fake Project Search")
7842 .with_project_items(&[
7843 dirty_regular_buffer.read(cx).project_items[0].clone(),
7844 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7845 clear_regular_buffer.read(cx).project_items[0].clone(),
7846 ])
7847 });
7848 workspace.update(cx, |workspace, cx| {
7849 workspace.add_item(
7850 pane.clone(),
7851 Box::new(dirty_regular_buffer.clone()),
7852 None,
7853 false,
7854 false,
7855 cx,
7856 );
7857 workspace.add_item(
7858 pane.clone(),
7859 Box::new(dirty_regular_buffer_2.clone()),
7860 None,
7861 false,
7862 false,
7863 cx,
7864 );
7865 workspace.add_item(
7866 pane.clone(),
7867 Box::new(dirty_multi_buffer.clone()),
7868 None,
7869 false,
7870 false,
7871 cx,
7872 );
7873 });
7874
7875 pane.update(cx, |pane, cx| {
7876 pane.activate_item(2, true, true, cx);
7877 assert_eq!(
7878 pane.active_item().unwrap().item_id(),
7879 dirty_multi_buffer.item_id(),
7880 "Should select the multi buffer in the pane"
7881 );
7882 });
7883 let close_multi_buffer_task = pane
7884 .update(cx, |pane, cx| {
7885 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7886 })
7887 .expect("should have active multi buffer to close");
7888 cx.background_executor.run_until_parked();
7889 assert!(
7890 !cx.has_pending_prompt(),
7891 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
7892 );
7893 close_multi_buffer_task
7894 .await
7895 .expect("Closing multi buffer failed");
7896 pane.update(cx, |pane, cx| {
7897 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7898 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
7899 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7900 assert_eq!(
7901 pane.items()
7902 .map(|item| item.item_id())
7903 .sorted()
7904 .collect::<Vec<_>>(),
7905 vec![
7906 dirty_regular_buffer.item_id(),
7907 dirty_regular_buffer_2.item_id(),
7908 ],
7909 "Should have no multi buffer left in the pane"
7910 );
7911 assert!(dirty_regular_buffer.read(cx).is_dirty);
7912 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
7913 });
7914 }
7915
7916 mod register_project_item_tests {
7917 use gpui::Context as _;
7918
7919 use super::*;
7920
7921 // View
7922 struct TestPngItemView {
7923 focus_handle: FocusHandle,
7924 }
7925 // Model
7926 struct TestPngItem {}
7927
7928 impl project::ProjectItem for TestPngItem {
7929 fn try_open(
7930 _project: &Model<Project>,
7931 path: &ProjectPath,
7932 cx: &mut AppContext,
7933 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7934 if path.path.extension().unwrap() == "png" {
7935 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7936 } else {
7937 None
7938 }
7939 }
7940
7941 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7942 None
7943 }
7944
7945 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7946 None
7947 }
7948
7949 fn is_dirty(&self) -> bool {
7950 false
7951 }
7952 }
7953
7954 impl Item for TestPngItemView {
7955 type Event = ();
7956 }
7957 impl EventEmitter<()> for TestPngItemView {}
7958 impl FocusableView for TestPngItemView {
7959 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7960 self.focus_handle.clone()
7961 }
7962 }
7963
7964 impl Render for TestPngItemView {
7965 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7966 Empty
7967 }
7968 }
7969
7970 impl ProjectItem for TestPngItemView {
7971 type Item = TestPngItem;
7972
7973 fn for_project_item(
7974 _project: Model<Project>,
7975 _item: Model<Self::Item>,
7976 cx: &mut ViewContext<Self>,
7977 ) -> Self
7978 where
7979 Self: Sized,
7980 {
7981 Self {
7982 focus_handle: cx.focus_handle(),
7983 }
7984 }
7985 }
7986
7987 // View
7988 struct TestIpynbItemView {
7989 focus_handle: FocusHandle,
7990 }
7991 // Model
7992 struct TestIpynbItem {}
7993
7994 impl project::ProjectItem for TestIpynbItem {
7995 fn try_open(
7996 _project: &Model<Project>,
7997 path: &ProjectPath,
7998 cx: &mut AppContext,
7999 ) -> Option<Task<gpui::Result<Model<Self>>>> {
8000 if path.path.extension().unwrap() == "ipynb" {
8001 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
8002 } else {
8003 None
8004 }
8005 }
8006
8007 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
8008 None
8009 }
8010
8011 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
8012 None
8013 }
8014
8015 fn is_dirty(&self) -> bool {
8016 false
8017 }
8018 }
8019
8020 impl Item for TestIpynbItemView {
8021 type Event = ();
8022 }
8023 impl EventEmitter<()> for TestIpynbItemView {}
8024 impl FocusableView for TestIpynbItemView {
8025 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8026 self.focus_handle.clone()
8027 }
8028 }
8029
8030 impl Render for TestIpynbItemView {
8031 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8032 Empty
8033 }
8034 }
8035
8036 impl ProjectItem for TestIpynbItemView {
8037 type Item = TestIpynbItem;
8038
8039 fn for_project_item(
8040 _project: Model<Project>,
8041 _item: Model<Self::Item>,
8042 cx: &mut ViewContext<Self>,
8043 ) -> Self
8044 where
8045 Self: Sized,
8046 {
8047 Self {
8048 focus_handle: cx.focus_handle(),
8049 }
8050 }
8051 }
8052
8053 struct TestAlternatePngItemView {
8054 focus_handle: FocusHandle,
8055 }
8056
8057 impl Item for TestAlternatePngItemView {
8058 type Event = ();
8059 }
8060
8061 impl EventEmitter<()> for TestAlternatePngItemView {}
8062 impl FocusableView for TestAlternatePngItemView {
8063 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8064 self.focus_handle.clone()
8065 }
8066 }
8067
8068 impl Render for TestAlternatePngItemView {
8069 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8070 Empty
8071 }
8072 }
8073
8074 impl ProjectItem for TestAlternatePngItemView {
8075 type Item = TestPngItem;
8076
8077 fn for_project_item(
8078 _project: Model<Project>,
8079 _item: Model<Self::Item>,
8080 cx: &mut ViewContext<Self>,
8081 ) -> Self
8082 where
8083 Self: Sized,
8084 {
8085 Self {
8086 focus_handle: cx.focus_handle(),
8087 }
8088 }
8089 }
8090
8091 #[gpui::test]
8092 async fn test_register_project_item(cx: &mut TestAppContext) {
8093 init_test(cx);
8094
8095 cx.update(|cx| {
8096 register_project_item::<TestPngItemView>(cx);
8097 register_project_item::<TestIpynbItemView>(cx);
8098 });
8099
8100 let fs = FakeFs::new(cx.executor());
8101 fs.insert_tree(
8102 "/root1",
8103 json!({
8104 "one.png": "BINARYDATAHERE",
8105 "two.ipynb": "{ totally a notebook }",
8106 "three.txt": "editing text, sure why not?"
8107 }),
8108 )
8109 .await;
8110
8111 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8112 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8113
8114 let worktree_id = project.update(cx, |project, cx| {
8115 project.worktrees(cx).next().unwrap().read(cx).id()
8116 });
8117
8118 let handle = workspace
8119 .update(cx, |workspace, cx| {
8120 let project_path = (worktree_id, "one.png");
8121 workspace.open_path(project_path, None, true, cx)
8122 })
8123 .await
8124 .unwrap();
8125
8126 // Now we can check if the handle we got back errored or not
8127 assert_eq!(
8128 handle.to_any().entity_type(),
8129 TypeId::of::<TestPngItemView>()
8130 );
8131
8132 let handle = workspace
8133 .update(cx, |workspace, cx| {
8134 let project_path = (worktree_id, "two.ipynb");
8135 workspace.open_path(project_path, None, true, cx)
8136 })
8137 .await
8138 .unwrap();
8139
8140 assert_eq!(
8141 handle.to_any().entity_type(),
8142 TypeId::of::<TestIpynbItemView>()
8143 );
8144
8145 let handle = workspace
8146 .update(cx, |workspace, cx| {
8147 let project_path = (worktree_id, "three.txt");
8148 workspace.open_path(project_path, None, true, cx)
8149 })
8150 .await;
8151 assert!(handle.is_err());
8152 }
8153
8154 #[gpui::test]
8155 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8156 init_test(cx);
8157
8158 cx.update(|cx| {
8159 register_project_item::<TestPngItemView>(cx);
8160 register_project_item::<TestAlternatePngItemView>(cx);
8161 });
8162
8163 let fs = FakeFs::new(cx.executor());
8164 fs.insert_tree(
8165 "/root1",
8166 json!({
8167 "one.png": "BINARYDATAHERE",
8168 "two.ipynb": "{ totally a notebook }",
8169 "three.txt": "editing text, sure why not?"
8170 }),
8171 )
8172 .await;
8173
8174 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8175 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8176
8177 let worktree_id = project.update(cx, |project, cx| {
8178 project.worktrees(cx).next().unwrap().read(cx).id()
8179 });
8180
8181 let handle = workspace
8182 .update(cx, |workspace, cx| {
8183 let project_path = (worktree_id, "one.png");
8184 workspace.open_path(project_path, None, true, cx)
8185 })
8186 .await
8187 .unwrap();
8188
8189 // This _must_ be the second item registered
8190 assert_eq!(
8191 handle.to_any().entity_type(),
8192 TypeId::of::<TestAlternatePngItemView>()
8193 );
8194
8195 let handle = workspace
8196 .update(cx, |workspace, cx| {
8197 let project_path = (worktree_id, "three.txt");
8198 workspace.open_path(project_path, None, true, cx)
8199 })
8200 .await;
8201 assert!(handle.is_err());
8202 }
8203 }
8204
8205 pub fn init_test(cx: &mut TestAppContext) {
8206 cx.update(|cx| {
8207 let settings_store = SettingsStore::test(cx);
8208 cx.set_global(settings_store);
8209 theme::init(theme::LoadThemes::JustBase, cx);
8210 language::init(cx);
8211 crate::init_settings(cx);
8212 Project::init_settings(cx);
8213 });
8214 }
8215
8216 fn dirty_project_item(id: u64, path: &str, cx: &mut AppContext) -> Model<TestProjectItem> {
8217 let item = TestProjectItem::new(id, path, cx);
8218 item.update(cx, |item, _| {
8219 item.is_dirty = true;
8220 });
8221 item
8222 }
8223}