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