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