diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-18 16:05:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-18 16:05:43 +0300 |
| commit | d74ba8ca9e49d9d94a4e59a0ee0bdbd1128af76e (patch) | |
| tree | 13473d5a8c3fd137f40e645e9f9eff3081ce3238 /src/app/api | |
| parent | b7a504b35e54e68ca7aceb01d21f066124f7a536 (diff) | |
fix: emit plugin lifecycle events from ui worktree flows
refs #662
Diffstat (limited to 'src/app/api')
| -rw-r--r-- | src/app/api/plugins/context.rs | 49 | ||||
| -rw-r--r-- | src/app/api/plugins/mod.rs | 70 | ||||
| -rw-r--r-- | src/app/api/tabs.rs | 15 | ||||
| -rw-r--r-- | src/app/api/workspaces.rs | 24 | ||||
| -rw-r--r-- | src/app/api/worktrees.rs | 148 |
5 files changed, 193 insertions, 113 deletions
diff --git a/src/app/api/plugins/context.rs b/src/app/api/plugins/context.rs index cd7d1818..92d0d4c6 100644 --- a/src/app/api/plugins/context.rs +++ b/src/app/api/plugins/context.rs @@ -63,14 +63,31 @@ impl App { }) }), EventData::WorkspaceRenamed { workspace_id, .. } - | EventData::WorkspaceFocused { workspace_id } - | EventData::WorktreeRemoved { workspace_id, .. } => self + | EventData::WorkspaceFocused { workspace_id } => self .plugin_context_for_workspace_id(workspace_id, correlation_id) .unwrap_or_else(|| { let mut context = empty_plugin_context(correlation_id); context.workspace_id = Some(workspace_id.clone()); context }), + EventData::WorktreeRemoved { + workspace_id, + workspace, + worktree, + .. + } => workspace + .as_ref() + .map(|workspace| { + self.plugin_context_for_workspace_snapshot(workspace, correlation_id) + }) + .or_else(|| self.plugin_context_for_workspace_id(workspace_id, correlation_id)) + .unwrap_or_else(|| { + let mut context = empty_plugin_context(correlation_id); + context.workspace_id = Some(workspace_id.clone()); + context.workspace_label = Some(worktree.label.clone()); + context.workspace_cwd = Some(worktree.path.clone()); + context + }), EventData::TabCreated { tab } => self.plugin_context_for_tab_info(tab, correlation_id), EventData::TabClosed { tab_id, @@ -168,19 +185,27 @@ impl App { ) -> PluginInvocationContext { self.plugin_context_for_workspace_id(&workspace.workspace_id, correlation_id) .unwrap_or_else(|| { - let mut context = empty_plugin_context(correlation_id); - context.workspace_id = Some(workspace.workspace_id.clone()); - context.workspace_label = Some(workspace.label.clone()); - context.workspace_cwd = workspace - .worktree - .as_ref() - .map(|worktree| worktree.checkout_path.clone()); - context.worktree = workspace.worktree.clone(); - context.tab_id = Some(workspace.active_tab_id.clone()); - context + self.plugin_context_for_workspace_snapshot(workspace, correlation_id) }) } + fn plugin_context_for_workspace_snapshot( + &self, + workspace: &crate::api::schema::WorkspaceInfo, + correlation_id: &str, + ) -> PluginInvocationContext { + let mut context = empty_plugin_context(correlation_id); + context.workspace_id = Some(workspace.workspace_id.clone()); + context.workspace_label = Some(workspace.label.clone()); + context.workspace_cwd = workspace + .worktree + .as_ref() + .map(|worktree| worktree.checkout_path.clone()); + context.worktree = workspace.worktree.clone(); + context.tab_id = Some(workspace.active_tab_id.clone()); + context + } + fn plugin_context_for_tab_id( &self, tab_id: &str, diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs index 8c0e4212..833134b5 100644 --- a/src/app/api/plugins/mod.rs +++ b/src/app/api/plugins/mod.rs @@ -1906,6 +1906,76 @@ command = ["sh", "-c", "printf '%s' \"$HERDR_PLUGIN_CONTEXT_JSON\" > {}"] pane_context.focused_pane_id.as_deref(), Some(active_public_pane_id.as_str()) ); + + app.state.workspaces[0].worktree_space = Some(crate::workspace::WorktreeSpaceMembership { + key: "repo-key".into(), + label: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: "/repo/herdr-issue".into(), + is_linked_worktree: true, + }); + let workspace = app.workspace_info(0); + let worktree = crate::api::schema::WorktreeInfo { + path: "/repo/herdr-issue".into(), + branch: Some("worktree/issue".into()), + is_bare: false, + is_detached: false, + is_prunable: false, + is_linked_worktree: true, + open_workspace_id: None, + label: "herdr".into(), + }; + app.state.workspaces[0].worktree_space = Some(crate::workspace::WorktreeSpaceMembership { + key: "repo-key".into(), + label: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: "/repo/herdr-other".into(), + is_linked_worktree: true, + }); + let changed_context = app.plugin_context_for_event( + &crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::WorktreeRemoved, + data: crate::api::schema::EventData::WorktreeRemoved { + workspace_id: workspace_id.clone(), + workspace: Some(workspace.clone()), + worktree: worktree.clone(), + forced: true, + }, + }, + "worktree.removed", + ); + assert_eq!( + changed_context + .worktree + .as_ref() + .map(|worktree| worktree.checkout_path.as_str()), + Some("/repo/herdr-issue") + ); + + app.state.workspaces.clear(); + let removed_context = app.plugin_context_for_event( + &crate::api::schema::EventEnvelope { + event: crate::api::schema::EventKind::WorktreeRemoved, + data: crate::api::schema::EventData::WorktreeRemoved { + workspace_id: workspace_id.clone(), + workspace: Some(workspace), + worktree, + forced: true, + }, + }, + "worktree.removed", + ); + assert_eq!( + removed_context.workspace_id.as_deref(), + Some(workspace_id.as_str()) + ); + assert_eq!( + removed_context + .worktree + .as_ref() + .map(|worktree| worktree.checkout_path.as_str()), + Some("/repo/herdr-issue") + ); } #[cfg(unix)] diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs index 079c2ea4..6c14a330 100644 --- a/src/app/api/tabs.rs +++ b/src/app/api/tabs.rs @@ -122,20 +122,7 @@ impl App { self.state.mode = Mode::Terminal; } self.schedule_session_save(); - let tab = self.tab_info(ws_idx, tab_idx).unwrap(); - let root_pane = self - .root_pane_info(ws_idx, tab_idx) - .expect("new tab should have a root pane"); - self.emit_event(EventEnvelope { - event: EventKind::TabCreated, - data: EventData::TabCreated { tab: tab.clone() }, - }); - self.emit_event(EventEnvelope { - event: EventKind::PaneCreated, - data: EventData::PaneCreated { - pane: root_pane.clone(), - }, - }); + self.emit_tab_created_events(ws_idx, tab_idx); encode_success( id, self.tab_created_result(ws_idx, tab_idx) diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs index cdfd4ec5..d1d7bf5c 100644 --- a/src/app/api/workspaces.rs +++ b/src/app/api/workspaces.rs @@ -63,29 +63,7 @@ impl App { crate::logging::workspace_renamed(&workspace.id); } } - let workspace = self.workspace_info(index); - let tab = self - .tab_info(index, 0) - .expect("new workspace should have an initial tab"); - let root_pane = self - .root_pane_info(index, 0) - .expect("new workspace should have an initial root pane"); - self.emit_event(EventEnvelope { - event: EventKind::WorkspaceCreated, - data: EventData::WorkspaceCreated { - workspace: workspace.clone(), - }, - }); - self.emit_event(EventEnvelope { - event: EventKind::TabCreated, - data: EventData::TabCreated { tab: tab.clone() }, - }); - self.emit_event(EventEnvelope { - event: EventKind::PaneCreated, - data: EventData::PaneCreated { - pane: root_pane.clone(), - }, - }); + self.emit_workspace_open_events(index); encode_success( id, self.workspace_created_result(index) diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs index 648b571a..abfa0c1e 100644 --- a/src/app/api/worktrees.rs +++ b/src/app/api/worktrees.rs @@ -146,15 +146,9 @@ impl App { self.emit_workspace_open_events(ws_idx); let worktree = self - .worktree_info_for_checkout(&source, ws_idx) + .worktree_info_for_workspace(ws_idx) .expect("created worktree workspace should have worktree info"); - self.emit_event(EventEnvelope { - event: EventKind::WorktreeCreated, - data: EventData::WorktreeCreated { - workspace: self.workspace_info(ws_idx), - worktree: worktree.clone(), - }, - }); + self.emit_worktree_created_event(ws_idx, worktree.clone()); encode_success( id, ResponseResult::WorktreeCreated { @@ -252,14 +246,7 @@ impl App { let tab_idx = self.state.workspaces[ws_idx].active_tab; let worktree = self.worktree_info_for_entry(&source, entry); - self.emit_event(EventEnvelope { - event: EventKind::WorktreeOpened, - data: EventData::WorktreeOpened { - workspace: self.workspace_info(ws_idx), - worktree: worktree.clone(), - already_open: already_open.is_some(), - }, - }); + self.emit_worktree_opened_event(ws_idx, worktree.clone(), already_open.is_some()); encode_success( id, ResponseResult::WorktreeOpened { @@ -355,18 +342,16 @@ impl App { event: EventKind::WorkspaceClosed, data: EventData::WorkspaceClosed { workspace_id: workspace_id.clone(), - workspace: Some(workspace_snapshot), + workspace: Some(workspace_snapshot.clone()), }, }); } - self.emit_event(EventEnvelope { - event: EventKind::WorktreeRemoved, - data: EventData::WorktreeRemoved { - workspace_id: workspace_id.clone(), - worktree, - forced: params.force, - }, - }); + self.emit_worktree_removed_event( + workspace_id.clone(), + Some(workspace_snapshot), + worktree, + params.force, + ); encode_success( id, @@ -653,7 +638,7 @@ impl App { self.set_worktree_membership(target_ws_idx, membership, emit_update); } - fn set_worktree_membership( + pub(crate) fn set_worktree_membership( &mut self, ws_idx: usize, membership: crate::workspace::WorktreeSpaceMembership, @@ -754,27 +739,7 @@ impl App { } } - fn worktree_info_for_checkout( - &self, - source: &WorktreeSource, - ws_idx: usize, - ) -> Option<WorktreeInfo> { - let membership = self.state.workspaces.get(ws_idx)?.worktree_space()?; - let branch = crate::workspace::git_branch(&membership.checkout_path); - let is_detached = branch.is_none(); - Some(WorktreeInfo { - path: membership.checkout_path.display().to_string(), - branch, - is_bare: false, - is_detached, - is_prunable: false, - is_linked_worktree: membership.is_linked_worktree, - open_workspace_id: Some(self.public_workspace_id(ws_idx)), - label: source.repo_name.clone(), - }) - } - - fn worktree_info_for_membership( + pub(crate) fn worktree_info_for_membership( &self, membership: &crate::workspace::WorktreeSpaceMembership, open_workspace_id: Option<String>, @@ -793,7 +758,7 @@ impl App { } } - fn open_workspace_idx_for_checkout(&self, checkout_path: &Path) -> Option<usize> { + pub(crate) fn open_workspace_idx_for_checkout(&self, checkout_path: &Path) -> Option<usize> { let canonical_checkout = crate::worktree::canonical_or_original(checkout_path); let checkout_key = canonical_checkout.display().to_string(); self.state.workspaces.iter().position(|ws| { @@ -823,27 +788,59 @@ impl App { }) } - fn emit_workspace_open_events(&mut self, ws_idx: usize) { - let workspace_info = self.workspace_info(ws_idx); - let Some(tab) = self.tab_info(ws_idx, 0) else { - return; - }; - let Some(root_pane) = self.root_pane_info(ws_idx, 0) else { - return; - }; + pub(crate) fn worktree_info_for_workspace(&self, ws_idx: usize) -> Option<WorktreeInfo> { + let membership = self.state.workspaces.get(ws_idx)?.worktree_space()?; + Some(self.worktree_info_for_membership(membership, Some(self.public_workspace_id(ws_idx)))) + } + + pub(crate) fn emit_worktree_created_event(&mut self, ws_idx: usize, worktree: WorktreeInfo) { self.emit_event(EventEnvelope { - event: EventKind::WorkspaceCreated, - data: EventData::WorkspaceCreated { - workspace: workspace_info, + event: EventKind::WorktreeCreated, + data: EventData::WorktreeCreated { + workspace: self.workspace_info(ws_idx), + worktree, }, }); + } + + pub(crate) fn emit_worktree_opened_for_workspace(&mut self, ws_idx: usize, already_open: bool) { + let Some(worktree) = self.worktree_info_for_workspace(ws_idx) else { + return; + }; + self.emit_worktree_opened_event(ws_idx, worktree, already_open); + } + + fn emit_worktree_opened_event( + &mut self, + ws_idx: usize, + worktree: WorktreeInfo, + already_open: bool, + ) { self.emit_event(EventEnvelope { - event: EventKind::TabCreated, - data: EventData::TabCreated { tab }, + event: EventKind::WorktreeOpened, + data: EventData::WorktreeOpened { + workspace: self.workspace_info(ws_idx), + worktree, + already_open, + }, }); + } + + pub(crate) fn emit_worktree_removed_event( + &mut self, + workspace_id: String, + workspace: Option<crate::api::schema::WorkspaceInfo>, + worktree: WorktreeInfo, + forced: bool, + ) { self.emit_event(EventEnvelope { - event: EventKind::PaneCreated, - data: EventData::PaneCreated { pane: root_pane }, + event: EventKind::WorktreeRemoved, + data: EventData::WorktreeRemoved { + workspace_id, + workspace, + worktree, + forced, + }, }); } @@ -1033,7 +1030,8 @@ mod tests { .is_linked_worktree ); assert!(workspace.worktree.unwrap().is_linked_worktree); - assert!(event_hub.events_after(0).iter().any(|(_, event)| { + let events = event_hub.events_after(0); + assert!(events.iter().any(|(_, event)| { matches!( &event.data, EventData::WorktreeCreated { @@ -1044,6 +1042,26 @@ mod tests { && event_worktree.is_linked_worktree ) })); + let kinds = events + .iter() + .map(|(_, event)| event.event) + .collect::<Vec<_>>(); + assert_eq!( + kinds + .iter() + .filter(|event| **event == EventKind::WorktreeCreated) + .count(), + 1 + ); + assert_eq!( + &kinds[kinds.len() - 4..], + &[ + EventKind::WorkspaceCreated, + EventKind::TabCreated, + EventKind::PaneCreated, + EventKind::WorktreeCreated, + ] + ); for (_, runtime) in app.terminal_runtimes.drain() { runtime.shutdown(); @@ -1674,9 +1692,11 @@ mod tests { &event.data, EventData::WorktreeRemoved { workspace_id, + workspace: Some(workspace), worktree, forced, } if workspace_id == &child_id + && workspace.workspace_id == child_id && worktree.branch.as_deref() == Some("worktree/api-remove-event") && worktree.is_linked_worktree && worktree.open_workspace_id.is_none() |
