From 8b74fb8a12716269c76f0b503ee7db5bbec2c921 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Mon, 29 Jun 2026 20:39:54 +0300 Subject: feat: emit placement mutation events --- src/app/api/layouts.rs | 6 +-- src/app/api/plugins/context.rs | 6 +++ src/app/api/tabs.rs | 76 ++++++++++++++++++++++------------- src/app/api/workspaces.rs | 90 +++++++++++++++++++++++++++++------------- 4 files changed, 120 insertions(+), 58 deletions(-) (limited to 'src/app/api') diff --git a/src/app/api/layouts.rs b/src/app/api/layouts.rs index 709d9f39..c7c02f5f 100644 --- a/src/app/api/layouts.rs +++ b/src/app/api/layouts.rs @@ -243,7 +243,7 @@ impl App { let Some(layout) = self.layout_description(ws_idx, tab_idx) else { return encode_error(id, "layout_not_found", "layout unavailable"); }; - encode_success(id, ResponseResult::LayoutApply { layout }) + encode_success(id, ResponseResult::LayoutSplitRatioSet { layout }) } fn resolve_layout_export_target(&self, params: &LayoutExportParams) -> Option<(usize, usize)> { @@ -684,8 +684,8 @@ mod tests { ); let success: SuccessResponse = serde_json::from_str(&response).unwrap(); - let ResponseResult::LayoutApply { layout } = success.result else { - panic!("expected layout apply response"); + let ResponseResult::LayoutSplitRatioSet { layout } = success.result else { + panic!("expected layout split ratio set response"); }; let LayoutNode::Split { ratio, .. } = layout.root else { panic!("expected split layout root"); diff --git a/src/app/api/plugins/context.rs b/src/app/api/plugins/context.rs index 92d0d4c6..e81a74d3 100644 --- a/src/app/api/plugins/context.rs +++ b/src/app/api/plugins/context.rs @@ -63,6 +63,7 @@ impl App { }) }), EventData::WorkspaceRenamed { workspace_id, .. } + | EventData::WorkspaceMoved { workspace_id, .. } | EventData::WorkspaceFocused { workspace_id } => self .plugin_context_for_workspace_id(workspace_id, correlation_id) .unwrap_or_else(|| { @@ -103,6 +104,11 @@ impl App { workspace_id, .. } + | EventData::TabMoved { + tab_id, + workspace_id, + .. + } | EventData::TabFocused { tab_id, workspace_id, diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs index 1ffcafdd..abd05b29 100644 --- a/src/app/api/tabs.rs +++ b/src/app/api/tabs.rs @@ -14,12 +14,10 @@ impl App { let Some(ws_idx) = self.parse_workspace_id(&workspace_id) else { return workspace_not_found(id, &workspace_id); }; - let Some(ws) = self.state.workspaces.get(ws_idx) else { + let Some(_) = self.state.workspaces.get(ws_idx) else { return workspace_not_found(id, &workspace_id); }; - (0..ws.tabs.len()) - .filter_map(|tab_idx| self.tab_info(ws_idx, tab_idx)) - .collect() + self.tab_list_info(ws_idx) } else { let mut tabs = Vec::new(); for (ws_idx, ws) in self.state.workspaces.iter().enumerate() { @@ -190,34 +188,35 @@ impl App { ); } + let tab_id = self + .public_tab_id(ws_idx, tab_idx) + .unwrap_or_else(|| crate::workspace::public_tab_id_for_number(&ws.id, tab_idx + 1)); + let workspace_id = self.public_workspace_id(ws_idx); + let insert_index = params.insert_index; let moved = self .state .workspaces .get_mut(ws_idx) - .is_some_and(|ws| ws.move_tab(tab_idx, params.insert_index)); + .is_some_and(|ws| ws.move_tab(tab_idx, insert_index)); + let tabs = self.tab_list_info(ws_idx); if moved { self.schedule_session_save(); if self.state.active == Some(ws_idx) { self.state.tab_scroll_follow_active = true; self.state.refresh_tab_bar_view(); } + self.emit_event(EventEnvelope { + event: EventKind::TabMoved, + data: EventData::TabMoved { + tab_id, + workspace_id, + insert_index, + tabs: tabs.clone(), + }, + }); } - encode_success( - id, - ResponseResult::TabList { - tabs: self - .state - .workspaces - .get(ws_idx) - .map(|ws| { - (0..ws.tabs.len()) - .filter_map(|idx| self.tab_info(ws_idx, idx)) - .collect() - }) - .unwrap_or_default(), - }, - ) + encode_success(id, ResponseResult::TabList { tabs }) } pub(super) fn handle_tab_close(&mut self, id: String, target: TabTarget) -> String { @@ -269,6 +268,18 @@ impl App { encode_success(id, ResponseResult::Ok {}) } + + fn tab_list_info(&self, ws_idx: usize) -> Vec { + self.state + .workspaces + .get(ws_idx) + .map(|ws| { + (0..ws.tabs.len()) + .filter_map(|idx| self.tab_info(ws_idx, idx)) + .collect() + }) + .unwrap_or_default() + } } fn workspace_not_found(id: String, workspace_id: &str) -> String { @@ -290,14 +301,9 @@ mod tests { #[test] fn api_tab_move_reorders_tabs_in_target_workspace() { + let event_hub = crate::api::EventHub::default(); let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut app = App::new( - &Config::default(), - true, - None, - api_rx, - crate::api::EventHub::default(), - ); + let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone()); let mut workspace = Workspace::test_new("tabs"); workspace.test_add_tab(Some("two")); workspace.test_add_tab(Some("three")); @@ -310,7 +316,7 @@ mod tests { let response = app.handle_tab_move( "req".into(), TabMoveParams { - tab_id: moved_id, + tab_id: moved_id.clone(), insert_index: 3, }, ); @@ -321,5 +327,19 @@ mod tests { }; assert_eq!(app.state.workspaces[0].tabs[2].root_pane, moved_root); assert_eq!(tabs[2].tab_id, app.public_tab_id(0, 2).unwrap()); + let events = event_hub.events_after(0); + assert!(events.iter().any(|(_, event)| { + matches!( + &event.data, + EventData::TabMoved { + tab_id, + workspace_id, + insert_index: 3, + tabs, + } if tab_id == &moved_id + && workspace_id == &app.public_workspace_id(0) + && tabs[2].tab_id == moved_id + ) + })); } } diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs index 56427463..f4189a04 100644 --- a/src/app/api/workspaces.rs +++ b/src/app/api/workspaces.rs @@ -13,13 +13,7 @@ impl App { encode_success( id, ResponseResult::WorkspaceList { - workspaces: self - .state - .workspaces - .iter() - .enumerate() - .map(|(idx, _)| self.workspace_info(idx)) - .collect(), + workspaces: self.workspace_list_info(), }, ) } @@ -140,20 +134,22 @@ impl App { ); } - self.state.move_workspace(index, params.insert_index); + let workspace_id = self.public_workspace_id(index); + let insert_index = params.insert_index; + let moved = self.state.move_workspace(index, insert_index); + let workspaces = self.workspace_list_info(); + if moved { + self.emit_event(EventEnvelope { + event: EventKind::WorkspaceMoved, + data: EventData::WorkspaceMoved { + workspace_id, + insert_index, + workspaces: workspaces.clone(), + }, + }); + } - encode_success( - id, - ResponseResult::WorkspaceList { - workspaces: self - .state - .workspaces - .iter() - .enumerate() - .map(|(idx, _)| self.workspace_info(idx)) - .collect(), - }, - ) + encode_success(id, ResponseResult::WorkspaceList { workspaces }) } pub(super) fn handle_workspace_close(&mut self, id: String, target: WorkspaceTarget) -> String { @@ -192,6 +188,15 @@ impl App { encode_success(id, ResponseResult::Ok {}) } + + fn workspace_list_info(&self) -> Vec { + self.state + .workspaces + .iter() + .enumerate() + .map(|(idx, _)| self.workspace_info(idx)) + .collect() + } } fn workspace_not_found(id: String, workspace_id: &str) -> String { @@ -279,14 +284,9 @@ mod tests { #[test] fn api_workspace_move_reorders_workspaces() { + let event_hub = crate::api::EventHub::default(); let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut app = App::new( - &Config::default(), - true, - None, - api_rx, - crate::api::EventHub::default(), - ); + let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone()); app.state.workspaces = vec![ Workspace::test_new("one"), Workspace::test_new("two"), @@ -310,5 +310,41 @@ mod tests { }; assert_eq!(workspaces[2].workspace_id, moved_id); assert_eq!(app.state.workspaces[2].display_name(), "one"); + let events = event_hub.events_after(0); + assert!(events.iter().any(|(_, event)| { + matches!( + &event.data, + EventData::WorkspaceMoved { + workspace_id, + insert_index: 3, + workspaces, + } if workspace_id == &moved_id + && workspaces[2].workspace_id == moved_id + ) + })); + } + + #[test] + fn api_workspace_move_noop_does_not_emit_event() { + let event_hub = crate::api::EventHub::default(); + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone()); + app.state.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")]; + let moved_id = app.public_workspace_id(0); + + let response = app.handle_workspace_move( + "req".into(), + WorkspaceMoveParams { + workspace_id: moved_id.clone(), + insert_index: 1, + }, + ); + + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + let ResponseResult::WorkspaceList { workspaces } = success.result else { + panic!("expected workspace list"); + }; + assert_eq!(workspaces[0].workspace_id, moved_id); + assert!(event_hub.events_after(0).is_empty()); } } -- cgit v1.2.3