This commit is contained in:
@@ -268,6 +268,13 @@ pub enum WorkspaceCommand {
|
||||
ratio: SplitRatio,
|
||||
placement: PanePlacement,
|
||||
},
|
||||
AttachPane {
|
||||
target: PaneId,
|
||||
pane: PaneId,
|
||||
axis: SplitAxis,
|
||||
ratio: SplitRatio,
|
||||
placement: PanePlacement,
|
||||
},
|
||||
SelectPane {
|
||||
pane: PaneId,
|
||||
},
|
||||
@@ -291,6 +298,7 @@ impl WorkspaceCommand {
|
||||
} => WorkspaceCapability::Execute,
|
||||
Self::SplitPane { pane, .. } if pane.launch.is_some() => WorkspaceCapability::Execute,
|
||||
Self::SplitPane { .. }
|
||||
| Self::AttachPane { .. }
|
||||
| Self::SelectPane { .. }
|
||||
| Self::RenamePane { .. }
|
||||
| Self::ClosePane {
|
||||
@@ -319,6 +327,10 @@ pub enum WorkspaceEvent {
|
||||
PaneSelected {
|
||||
pane: PaneId,
|
||||
},
|
||||
PaneAttached {
|
||||
target: PaneId,
|
||||
pane: PaneId,
|
||||
},
|
||||
PaneRenamed {
|
||||
pane: PaneId,
|
||||
},
|
||||
@@ -366,6 +378,7 @@ pub struct WorkspaceState {
|
||||
id: WorkspaceId,
|
||||
name: String,
|
||||
panes: BTreeMap<PaneId, PaneDefinition>,
|
||||
detached_panes: BTreeMap<PaneId, PaneDefinition>,
|
||||
layout: LayoutNode,
|
||||
selected: PaneId,
|
||||
applied_requests: BTreeMap<CommandRequestId, WorkspaceRequest>,
|
||||
@@ -396,6 +409,7 @@ impl WorkspaceState {
|
||||
id,
|
||||
name,
|
||||
panes,
|
||||
detached_panes: BTreeMap::new(),
|
||||
layout,
|
||||
selected,
|
||||
applied_requests: BTreeMap::new(),
|
||||
@@ -433,11 +447,21 @@ impl WorkspaceState {
|
||||
self.panes.get(id)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn detached_pane(&self, id: &PaneId) -> Option<&PaneDefinition> {
|
||||
self.detached_panes.get(id)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pane_count(&self) -> usize {
|
||||
self.panes.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn detached_pane_count(&self) -> usize {
|
||||
self.detached_panes.len()
|
||||
}
|
||||
|
||||
/// Applies one idempotent command after checking its required capability.
|
||||
///
|
||||
/// # Errors
|
||||
@@ -511,7 +535,7 @@ impl WorkspaceState {
|
||||
if !self.layout.contains(&target) {
|
||||
return Err(WorkspaceError::PaneNotFound(target));
|
||||
}
|
||||
if self.panes.contains_key(&pane.id) {
|
||||
if self.panes.contains_key(&pane.id) || self.detached_panes.contains_key(&pane.id) {
|
||||
return Err(WorkspaceError::PaneAlreadyExists(pane.id));
|
||||
}
|
||||
pane.validate()?;
|
||||
@@ -527,6 +551,31 @@ impl WorkspaceState {
|
||||
pane: pane_id,
|
||||
})
|
||||
}
|
||||
WorkspaceCommand::AttachPane {
|
||||
target,
|
||||
pane,
|
||||
axis,
|
||||
ratio,
|
||||
placement,
|
||||
} => {
|
||||
if !self.layout.contains(&target) {
|
||||
return Err(WorkspaceError::PaneNotFound(target));
|
||||
}
|
||||
if self.panes.contains_key(&pane) {
|
||||
return Err(WorkspaceError::PaneAlreadyExists(pane));
|
||||
}
|
||||
let definition = self
|
||||
.detached_panes
|
||||
.remove(&pane)
|
||||
.ok_or_else(|| WorkspaceError::PaneNotFound(pane.clone()))?;
|
||||
let did_split = self
|
||||
.layout
|
||||
.split(&target, pane.clone(), axis, ratio, placement);
|
||||
debug_assert!(did_split, "target presence was checked");
|
||||
self.panes.insert(pane.clone(), definition);
|
||||
self.selected = pane.clone();
|
||||
Ok(WorkspaceEvent::PaneAttached { target, pane })
|
||||
}
|
||||
WorkspaceCommand::SelectPane { pane } => {
|
||||
if !self.panes.contains_key(&pane) {
|
||||
return Err(WorkspaceError::PaneNotFound(pane));
|
||||
@@ -546,6 +595,11 @@ impl WorkspaceState {
|
||||
Ok(WorkspaceEvent::PaneRenamed { pane })
|
||||
}
|
||||
WorkspaceCommand::ClosePane { pane, disposition } => {
|
||||
if disposition == PaneCloseDisposition::Terminate
|
||||
&& self.detached_panes.remove(&pane).is_some()
|
||||
{
|
||||
return Ok(WorkspaceEvent::PaneClosed { pane, disposition });
|
||||
}
|
||||
if self.panes.len() == 1 && self.panes.contains_key(&pane) {
|
||||
return Err(WorkspaceError::CannotCloseLastPane);
|
||||
}
|
||||
@@ -557,7 +611,10 @@ impl WorkspaceState {
|
||||
.clone()
|
||||
.remove(&pane)
|
||||
.ok_or(WorkspaceError::CannotCloseLastPane)?;
|
||||
self.panes.remove(&pane);
|
||||
let definition = self.panes.remove(&pane).expect("pane presence was checked");
|
||||
if disposition == PaneCloseDisposition::Detach {
|
||||
self.detached_panes.insert(pane.clone(), definition);
|
||||
}
|
||||
self.layout = layout;
|
||||
if self.selected == pane {
|
||||
self.selected = self.layout.first_pane().clone();
|
||||
@@ -580,6 +637,11 @@ impl fmt::Debug for WorkspaceEvent {
|
||||
.debug_struct("PaneSelected")
|
||||
.field("pane", pane)
|
||||
.finish(),
|
||||
Self::PaneAttached { target, pane } => formatter
|
||||
.debug_struct("PaneAttached")
|
||||
.field("target", target)
|
||||
.field("pane", pane)
|
||||
.finish(),
|
||||
Self::PaneRenamed { pane } => formatter
|
||||
.debug_struct("PaneRenamed")
|
||||
.field("pane", pane)
|
||||
@@ -828,6 +890,8 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(state.selected().as_str(), "root");
|
||||
assert_eq!(state.pane_count(), 1);
|
||||
assert_eq!(state.detached_pane_count(), 1);
|
||||
assert!(state.detached_pane(&id(PaneId::new, "second")).is_some());
|
||||
assert!(matches!(
|
||||
state.apply(
|
||||
request(
|
||||
@@ -843,6 +907,110 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_pane_can_be_reattached_without_a_new_launch() {
|
||||
let mut state = workspace();
|
||||
state
|
||||
.apply(
|
||||
request(
|
||||
"split",
|
||||
WorkspaceCommand::SplitPane {
|
||||
target: id(PaneId::new, "root"),
|
||||
pane: pane("agent", true),
|
||||
axis: SplitAxis::Horizontal,
|
||||
ratio: SplitRatio::default(),
|
||||
placement: PanePlacement::After,
|
||||
},
|
||||
),
|
||||
WorkspaceCapability::Execute,
|
||||
)
|
||||
.unwrap();
|
||||
state
|
||||
.apply(
|
||||
request(
|
||||
"detach",
|
||||
WorkspaceCommand::ClosePane {
|
||||
pane: id(PaneId::new, "agent"),
|
||||
disposition: PaneCloseDisposition::Detach,
|
||||
},
|
||||
),
|
||||
WorkspaceCapability::Configure,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.pane_count(), 1);
|
||||
assert_eq!(state.detached_pane_count(), 1);
|
||||
state
|
||||
.apply(
|
||||
request(
|
||||
"attach",
|
||||
WorkspaceCommand::AttachPane {
|
||||
target: id(PaneId::new, "root"),
|
||||
pane: id(PaneId::new, "agent"),
|
||||
axis: SplitAxis::Horizontal,
|
||||
ratio: SplitRatio::default(),
|
||||
placement: PanePlacement::After,
|
||||
},
|
||||
),
|
||||
WorkspaceCapability::Configure,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.pane_count(), 2);
|
||||
assert_eq!(state.detached_pane_count(), 0);
|
||||
assert_eq!(state.selected().as_str(), "agent");
|
||||
assert!(state.pane(&id(PaneId::new, "agent")).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminating_a_detached_pane_requires_execute_and_removes_it() {
|
||||
let mut state = workspace();
|
||||
state
|
||||
.apply(
|
||||
request(
|
||||
"split",
|
||||
WorkspaceCommand::SplitPane {
|
||||
target: id(PaneId::new, "root"),
|
||||
pane: pane("agent", false),
|
||||
axis: SplitAxis::Horizontal,
|
||||
ratio: SplitRatio::default(),
|
||||
placement: PanePlacement::After,
|
||||
},
|
||||
),
|
||||
WorkspaceCapability::Configure,
|
||||
)
|
||||
.unwrap();
|
||||
state
|
||||
.apply(
|
||||
request(
|
||||
"detach",
|
||||
WorkspaceCommand::ClosePane {
|
||||
pane: id(PaneId::new, "agent"),
|
||||
disposition: PaneCloseDisposition::Detach,
|
||||
},
|
||||
),
|
||||
WorkspaceCapability::Configure,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let terminate = request(
|
||||
"terminate",
|
||||
WorkspaceCommand::ClosePane {
|
||||
pane: id(PaneId::new, "agent"),
|
||||
disposition: PaneCloseDisposition::Terminate,
|
||||
},
|
||||
);
|
||||
assert!(matches!(
|
||||
state.apply(terminate.clone(), WorkspaceCapability::Configure),
|
||||
Err(WorkspaceError::CapabilityDenied { .. })
|
||||
));
|
||||
state
|
||||
.apply(terminate, WorkspaceCapability::Execute)
|
||||
.unwrap();
|
||||
assert_eq!(state.detached_pane_count(), 0);
|
||||
assert_eq!(state.pane_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_identifiers_ratios_and_launch_surfaces() {
|
||||
assert!(PaneId::new(" ").is_err());
|
||||
|
||||
Reference in New Issue
Block a user