1use anyhow::Result;
2use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
3use gpui::{
4 AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
5 ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
6 actions, deferred, px,
7};
8use project::Project;
9use std::future::Future;
10use std::path::PathBuf;
11use ui::prelude::*;
12use util::ResultExt;
13
14const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
15
16use crate::{
17 DockPosition, Item, ModalView, Panel, Toast, Workspace, WorkspaceId, client_side_decorations,
18 notifications::NotificationId,
19};
20
21actions!(
22 multi_workspace,
23 [
24 /// Creates a new workspace within the current window.
25 NewWorkspaceInWindow,
26 /// Switches to the next workspace within the current window.
27 NextWorkspaceInWindow,
28 /// Switches to the previous workspace within the current window.
29 PreviousWorkspaceInWindow,
30 /// Toggles the workspace switcher sidebar.
31 ToggleWorkspaceSidebar,
32 /// Moves focus to or from the workspace sidebar without closing it.
33 FocusWorkspaceSidebar,
34 ]
35);
36
37pub enum SidebarEvent {
38 Open,
39 Close,
40}
41
42pub trait Sidebar: EventEmitter<SidebarEvent> + Focusable + Render + Sized {
43 fn width(&self, cx: &App) -> Pixels;
44 fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
45 fn has_notifications(&self, cx: &App) -> bool;
46}
47
48pub trait SidebarHandle: 'static + Send + Sync {
49 fn width(&self, cx: &App) -> Pixels;
50 fn set_width(&self, width: Option<Pixels>, cx: &mut App);
51 fn focus_handle(&self, cx: &App) -> FocusHandle;
52 fn focus(&self, window: &mut Window, cx: &mut App);
53 fn has_notifications(&self, cx: &App) -> bool;
54 fn to_any(&self) -> AnyView;
55 fn entity_id(&self) -> EntityId;
56}
57
58#[derive(Clone)]
59pub struct DraggedSidebar;
60
61impl Render for DraggedSidebar {
62 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
63 gpui::Empty
64 }
65}
66
67impl<T: Sidebar> SidebarHandle for Entity<T> {
68 fn width(&self, cx: &App) -> Pixels {
69 self.read(cx).width(cx)
70 }
71
72 fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
73 self.update(cx, |this, cx| this.set_width(width, cx))
74 }
75
76 fn focus_handle(&self, cx: &App) -> FocusHandle {
77 self.read(cx).focus_handle(cx)
78 }
79
80 fn focus(&self, window: &mut Window, cx: &mut App) {
81 let handle = self.read(cx).focus_handle(cx);
82 window.focus(&handle, cx);
83 }
84
85 fn has_notifications(&self, cx: &App) -> bool {
86 self.read(cx).has_notifications(cx)
87 }
88
89 fn to_any(&self) -> AnyView {
90 self.clone().into()
91 }
92
93 fn entity_id(&self) -> EntityId {
94 Entity::entity_id(self)
95 }
96}
97
98pub struct MultiWorkspace {
99 window_id: WindowId,
100 workspaces: Vec<Entity<Workspace>>,
101 active_workspace_index: usize,
102 sidebar: Option<Box<dyn SidebarHandle>>,
103 sidebar_open: bool,
104 _sidebar_subscription: Option<Subscription>,
105 pending_removal_tasks: Vec<Task<()>>,
106 _serialize_task: Option<Task<()>>,
107 _create_task: Option<Task<()>>,
108 _subscriptions: Vec<Subscription>,
109}
110
111impl MultiWorkspace {
112 pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
113 let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
114 if let Some(task) = this._serialize_task.take() {
115 task.detach();
116 }
117 if let Some(task) = this._create_task.take() {
118 task.detach();
119 }
120 for task in std::mem::take(&mut this.pending_removal_tasks) {
121 task.detach();
122 }
123 });
124 let quit_subscription = cx.on_app_quit(Self::app_will_quit);
125 Self {
126 window_id: window.window_handle().window_id(),
127 workspaces: vec![workspace],
128 active_workspace_index: 0,
129 sidebar: None,
130 sidebar_open: false,
131 _sidebar_subscription: None,
132 pending_removal_tasks: Vec::new(),
133 _serialize_task: None,
134 _create_task: None,
135 _subscriptions: vec![release_subscription, quit_subscription],
136 }
137 }
138
139 pub fn register_sidebar<T: Sidebar>(
140 &mut self,
141 sidebar: Entity<T>,
142 window: &mut Window,
143 cx: &mut Context<Self>,
144 ) {
145 let subscription =
146 cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event {
147 SidebarEvent::Open => this.toggle_sidebar(window, cx),
148 SidebarEvent::Close => {
149 this.close_sidebar(window, cx);
150 }
151 });
152 self.sidebar = Some(Box::new(sidebar));
153 self._sidebar_subscription = Some(subscription);
154 }
155
156 pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
157 self.sidebar.as_deref()
158 }
159
160 pub fn sidebar_open(&self) -> bool {
161 self.sidebar_open && self.sidebar.is_some()
162 }
163
164 pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
165 self.sidebar
166 .as_ref()
167 .map_or(false, |s| s.has_notifications(cx))
168 }
169
170 pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
171 cx.has_flag::<AgentV2FeatureFlag>()
172 }
173
174 pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
175 if !self.multi_workspace_enabled(cx) {
176 return;
177 }
178
179 if self.sidebar_open {
180 self.close_sidebar(window, cx);
181 } else {
182 self.open_sidebar(cx);
183 if let Some(sidebar) = &self.sidebar {
184 sidebar.focus(window, cx);
185 }
186 }
187 }
188
189 pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
190 if !self.multi_workspace_enabled(cx) {
191 return;
192 }
193
194 if self.sidebar_open {
195 let sidebar_is_focused = self
196 .sidebar
197 .as_ref()
198 .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
199
200 if sidebar_is_focused {
201 let pane = self.workspace().read(cx).active_pane().clone();
202 let pane_focus = pane.read(cx).focus_handle(cx);
203 window.focus(&pane_focus, cx);
204 } else if let Some(sidebar) = &self.sidebar {
205 sidebar.focus(window, cx);
206 }
207 } else {
208 self.open_sidebar(cx);
209 if let Some(sidebar) = &self.sidebar {
210 sidebar.focus(window, cx);
211 }
212 }
213 }
214
215 pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
216 self.sidebar_open = true;
217 for workspace in &self.workspaces {
218 workspace.update(cx, |workspace, cx| {
219 workspace.set_workspace_sidebar_open(true, cx);
220 });
221 }
222 self.serialize(cx);
223 cx.notify();
224 }
225
226 fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
227 self.sidebar_open = false;
228 for workspace in &self.workspaces {
229 workspace.update(cx, |workspace, cx| {
230 workspace.set_workspace_sidebar_open(false, cx);
231 });
232 }
233 let pane = self.workspace().read(cx).active_pane().clone();
234 let pane_focus = pane.read(cx).focus_handle(cx);
235 window.focus(&pane_focus, cx);
236 self.serialize(cx);
237 cx.notify();
238 }
239
240 pub fn is_sidebar_open(&self) -> bool {
241 self.sidebar_open
242 }
243
244 pub fn workspace(&self) -> &Entity<Workspace> {
245 &self.workspaces[self.active_workspace_index]
246 }
247
248 pub fn workspaces(&self) -> &[Entity<Workspace>] {
249 &self.workspaces
250 }
251
252 pub fn active_workspace_index(&self) -> usize {
253 self.active_workspace_index
254 }
255
256 pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
257 if !self.multi_workspace_enabled(cx) {
258 self.workspaces[0] = workspace;
259 self.active_workspace_index = 0;
260 cx.notify();
261 return;
262 }
263
264 let old_index = self.active_workspace_index;
265 let new_index = self.set_active_workspace(workspace, cx);
266 if old_index != new_index {
267 self.serialize(cx);
268 }
269 }
270
271 fn set_active_workspace(
272 &mut self,
273 workspace: Entity<Workspace>,
274 cx: &mut Context<Self>,
275 ) -> usize {
276 let index = self.add_workspace(workspace, cx);
277 self.active_workspace_index = index;
278 cx.notify();
279 index
280 }
281
282 /// Adds a workspace to this window without changing which workspace is active.
283 /// Returns the index of the workspace (existing or newly inserted).
284 pub fn add_workspace(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
285 if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
286 index
287 } else {
288 if self.sidebar_open {
289 workspace.update(cx, |workspace, cx| {
290 workspace.set_workspace_sidebar_open(true, cx);
291 });
292 }
293 self.workspaces.push(workspace);
294 cx.notify();
295 self.workspaces.len() - 1
296 }
297 }
298
299 pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
300 debug_assert!(
301 index < self.workspaces.len(),
302 "workspace index out of bounds"
303 );
304 self.active_workspace_index = index;
305 self.serialize(cx);
306 self.focus_active_workspace(window, cx);
307 cx.notify();
308 }
309
310 pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
311 if self.workspaces.len() > 1 {
312 let next_index = (self.active_workspace_index + 1) % self.workspaces.len();
313 self.activate_index(next_index, window, cx);
314 }
315 }
316
317 pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
318 if self.workspaces.len() > 1 {
319 let prev_index = if self.active_workspace_index == 0 {
320 self.workspaces.len() - 1
321 } else {
322 self.active_workspace_index - 1
323 };
324 self.activate_index(prev_index, window, cx);
325 }
326 }
327
328 fn serialize(&mut self, cx: &mut App) {
329 let window_id = self.window_id;
330 let state = crate::persistence::model::MultiWorkspaceState {
331 active_workspace_id: self.workspace().read(cx).database_id(),
332 sidebar_open: self.sidebar_open,
333 };
334 self._serialize_task = Some(cx.background_spawn(async move {
335 crate::persistence::write_multi_workspace_state(window_id, state).await;
336 }));
337 }
338
339 /// Returns the in-flight serialization task (if any) so the caller can
340 /// await it. Used by the quit handler to ensure pending DB writes
341 /// complete before the process exits.
342 pub fn flush_serialization(&mut self) -> Task<()> {
343 self._serialize_task.take().unwrap_or(Task::ready(()))
344 }
345
346 fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
347 let mut tasks: Vec<Task<()>> = Vec::new();
348 if let Some(task) = self._serialize_task.take() {
349 tasks.push(task);
350 }
351 if let Some(task) = self._create_task.take() {
352 tasks.push(task);
353 }
354 tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
355
356 async move {
357 futures::future::join_all(tasks).await;
358 }
359 }
360
361 fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
362 // If a dock panel is zoomed, focus it instead of the center pane.
363 // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
364 // which closes the zoomed dock.
365 let focus_handle = {
366 let workspace = self.workspace().read(cx);
367 let mut target = None;
368 for dock in workspace.all_docks() {
369 let dock = dock.read(cx);
370 if dock.is_open() {
371 if let Some(panel) = dock.active_panel() {
372 if panel.is_zoomed(window, cx) {
373 target = Some(panel.panel_focus_handle(cx));
374 break;
375 }
376 }
377 }
378 }
379 target.unwrap_or_else(|| {
380 let pane = workspace.active_pane().clone();
381 pane.read(cx).focus_handle(cx)
382 })
383 };
384 window.focus(&focus_handle, cx);
385 }
386
387 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
388 self.workspace().read(cx).panel::<T>(cx)
389 }
390
391 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
392 self.workspace().read(cx).active_modal::<V>(cx)
393 }
394
395 pub fn add_panel<T: Panel>(
396 &mut self,
397 panel: Entity<T>,
398 window: &mut Window,
399 cx: &mut Context<Self>,
400 ) {
401 self.workspace().update(cx, |workspace, cx| {
402 workspace.add_panel(panel, window, cx);
403 });
404 }
405
406 pub fn focus_panel<T: Panel>(
407 &mut self,
408 window: &mut Window,
409 cx: &mut Context<Self>,
410 ) -> Option<Entity<T>> {
411 self.workspace()
412 .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
413 }
414
415 pub fn toggle_modal<V: ModalView, B>(
416 &mut self,
417 window: &mut Window,
418 cx: &mut Context<Self>,
419 build: B,
420 ) where
421 B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
422 {
423 self.workspace().update(cx, |workspace, cx| {
424 workspace.toggle_modal(window, cx, build);
425 });
426 }
427
428 pub fn toggle_dock(
429 &mut self,
430 dock_side: DockPosition,
431 window: &mut Window,
432 cx: &mut Context<Self>,
433 ) {
434 self.workspace().update(cx, |workspace, cx| {
435 workspace.toggle_dock(dock_side, window, cx);
436 });
437 }
438
439 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
440 self.workspace().read(cx).active_item_as::<I>(cx)
441 }
442
443 pub fn items_of_type<'a, T: Item>(
444 &'a self,
445 cx: &'a App,
446 ) -> impl 'a + Iterator<Item = Entity<T>> {
447 self.workspace().read(cx).items_of_type::<T>(cx)
448 }
449
450 pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
451 self.workspace().read(cx).database_id()
452 }
453
454 pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
455 let mut tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
456 .into_iter()
457 .filter(|task| !task.is_ready())
458 .collect();
459 if let Some(task) = self._create_task.take() {
460 if !task.is_ready() {
461 tasks.push(task);
462 }
463 }
464 tasks
465 }
466
467 #[cfg(any(test, feature = "test-support"))]
468 pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
469 self.workspace().update(cx, |workspace, _cx| {
470 workspace.set_random_database_id();
471 });
472 }
473
474 #[cfg(any(test, feature = "test-support"))]
475 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
476 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
477 Self::new(workspace, window, cx)
478 }
479
480 #[cfg(any(test, feature = "test-support"))]
481 pub fn test_add_workspace(
482 &mut self,
483 project: Entity<Project>,
484 window: &mut Window,
485 cx: &mut Context<Self>,
486 ) -> Entity<Workspace> {
487 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
488 self.activate(workspace.clone(), cx);
489 workspace
490 }
491
492 pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
493 if !self.multi_workspace_enabled(cx) {
494 return;
495 }
496 let app_state = self.workspace().read(cx).app_state().clone();
497 let project = Project::local(
498 app_state.client.clone(),
499 app_state.node_runtime.clone(),
500 app_state.user_store.clone(),
501 app_state.languages.clone(),
502 app_state.fs.clone(),
503 None,
504 project::LocalProjectFlags::default(),
505 cx,
506 );
507 let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
508 self.set_active_workspace(new_workspace.clone(), cx);
509 self.focus_active_workspace(window, cx);
510
511 let weak_workspace = new_workspace.downgrade();
512 self._create_task = Some(cx.spawn_in(window, async move |this, cx| {
513 let result = crate::persistence::DB.next_id().await;
514 this.update_in(cx, |this, window, cx| match result {
515 Ok(workspace_id) => {
516 if let Some(workspace) = weak_workspace.upgrade() {
517 let session_id = workspace.read(cx).session_id();
518 let window_id = window.window_handle().window_id().as_u64();
519 workspace.update(cx, |workspace, _cx| {
520 workspace.set_database_id(workspace_id);
521 });
522 cx.background_spawn(async move {
523 crate::persistence::DB
524 .set_session_binding(workspace_id, session_id, Some(window_id))
525 .await
526 .log_err();
527 })
528 .detach();
529 } else {
530 cx.background_spawn(async move {
531 crate::persistence::DB
532 .delete_workspace_by_id(workspace_id)
533 .await
534 .log_err();
535 })
536 .detach();
537 }
538 this.serialize(cx);
539 }
540 Err(error) => {
541 log::error!("Failed to create workspace: {error:#}");
542 if let Some(index) = weak_workspace
543 .upgrade()
544 .and_then(|w| this.workspaces.iter().position(|ws| *ws == w))
545 {
546 this.remove_workspace(index, window, cx);
547 }
548 this.workspace().update(cx, |workspace, cx| {
549 let id = NotificationId::unique::<MultiWorkspace>();
550 workspace.show_toast(
551 Toast::new(id, format!("Failed to create workspace: {error}")),
552 cx,
553 );
554 });
555 }
556 })
557 .log_err();
558 }));
559 }
560
561 pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
562 if self.workspaces.len() <= 1 || index >= self.workspaces.len() {
563 return;
564 }
565
566 let removed_workspace = self.workspaces.remove(index);
567
568 if self.active_workspace_index >= self.workspaces.len() {
569 self.active_workspace_index = self.workspaces.len() - 1;
570 } else if self.active_workspace_index > index {
571 self.active_workspace_index -= 1;
572 }
573
574 if let Some(workspace_id) = removed_workspace.read(cx).database_id() {
575 self.pending_removal_tasks.retain(|task| !task.is_ready());
576 self.pending_removal_tasks
577 .push(cx.background_spawn(async move {
578 crate::persistence::DB
579 .delete_workspace_by_id(workspace_id)
580 .await
581 .log_err();
582 }));
583 }
584
585 self.serialize(cx);
586 self.focus_active_workspace(window, cx);
587 cx.notify();
588 }
589
590 pub fn open_project(
591 &mut self,
592 paths: Vec<PathBuf>,
593 window: &mut Window,
594 cx: &mut Context<Self>,
595 ) -> Task<Result<()>> {
596 let workspace = self.workspace().clone();
597
598 if self.multi_workspace_enabled(cx) {
599 workspace.update(cx, |workspace, cx| {
600 workspace.open_workspace_for_paths(true, paths, window, cx)
601 })
602 } else {
603 cx.spawn_in(window, async move |_this, cx| {
604 let should_continue = workspace
605 .update_in(cx, |workspace, window, cx| {
606 workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
607 })?
608 .await?;
609 if should_continue {
610 workspace
611 .update_in(cx, |workspace, window, cx| {
612 workspace.open_workspace_for_paths(true, paths, window, cx)
613 })?
614 .await
615 } else {
616 Ok(())
617 }
618 })
619 }
620 }
621}
622
623impl Render for MultiWorkspace {
624 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
625 let multi_workspace_enabled = self.multi_workspace_enabled(cx);
626
627 let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open {
628 self.sidebar.as_ref().map(|sidebar_handle| {
629 let weak = cx.weak_entity();
630
631 let sidebar_width = sidebar_handle.width(cx);
632 let resize_handle = deferred(
633 div()
634 .id("sidebar-resize-handle")
635 .absolute()
636 .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
637 .top(px(0.))
638 .h_full()
639 .w(SIDEBAR_RESIZE_HANDLE_SIZE)
640 .cursor_col_resize()
641 .on_drag(DraggedSidebar, |dragged, _, _, cx| {
642 cx.stop_propagation();
643 cx.new(|_| dragged.clone())
644 })
645 .on_mouse_down(MouseButton::Left, |_, _, cx| {
646 cx.stop_propagation();
647 })
648 .on_mouse_up(MouseButton::Left, move |event, _, cx| {
649 if event.click_count == 2 {
650 weak.update(cx, |this, cx| {
651 if let Some(sidebar) = this.sidebar.as_mut() {
652 sidebar.set_width(None, cx);
653 }
654 })
655 .ok();
656 cx.stop_propagation();
657 }
658 })
659 .occlude(),
660 );
661
662 div()
663 .id("sidebar-container")
664 .relative()
665 .h_full()
666 .w(sidebar_width)
667 .flex_shrink_0()
668 .child(sidebar_handle.to_any())
669 .child(resize_handle)
670 .into_any_element()
671 })
672 } else {
673 None
674 };
675
676 client_side_decorations(
677 h_flex()
678 .key_context("Workspace")
679 .size_full()
680 .on_action(
681 cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| {
682 this.create_workspace(window, cx);
683 }),
684 )
685 .on_action(
686 cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| {
687 this.activate_next_workspace(window, cx);
688 }),
689 )
690 .on_action(cx.listener(
691 |this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| {
692 this.activate_previous_workspace(window, cx);
693 },
694 ))
695 .on_action(cx.listener(
696 |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
697 this.toggle_sidebar(window, cx);
698 },
699 ))
700 .on_action(
701 cx.listener(|this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
702 this.focus_sidebar(window, cx);
703 }),
704 )
705 .when(
706 self.sidebar_open() && self.multi_workspace_enabled(cx),
707 |this| {
708 this.on_drag_move(cx.listener(
709 |this: &mut Self, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
710 if let Some(sidebar) = &this.sidebar {
711 let new_width = e.event.position.x;
712 sidebar.set_width(Some(new_width), cx);
713 }
714 },
715 ))
716 .children(sidebar)
717 },
718 )
719 .child(
720 div()
721 .flex()
722 .flex_1()
723 .size_full()
724 .overflow_hidden()
725 .child(self.workspace().clone()),
726 ),
727 window,
728 cx,
729 Tiling {
730 left: multi_workspace_enabled && self.sidebar_open,
731 ..Tiling::default()
732 },
733 )
734 }
735}