Transparent 'ZOOM/'PAN run nested and resume the interrupted command (fixes #220) (#221)

Squash of PR #221 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein
2026-10-11 11:12:47 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent c35a952271
commit 4e505a6adb
2 changed files with 98 additions and 4 deletions
+49 -4
View File
@@ -463,6 +463,9 @@ pub struct Session {
pub active: usize,
pub settings: Settings,
pub running: Option<Running>,
/// The command suspended while a transparent command (`'ZOOM`, `'PAN`…) prompts inside it;
/// it resumes when that command ends. One level, as in AutoCAD.
pub suspended: Option<Running>,
pub log: Vec<String>,
pub last_command: Option<String>,
/// LASTPOINT.
@@ -510,6 +513,7 @@ impl Session {
active: 0,
settings: Settings::default(),
running: None,
suspended: None,
log: Vec::new(),
last_command: None,
last_point: Vec2::ZERO,
@@ -698,19 +702,32 @@ impl Session {
self.echo(format!(">>{}", spec.label));
return self.execute(spec.id, &Value::Null).map(|_| ());
}
if self.suspended.is_some() {
self.echo("A transparent command is already in progress.");
return Ok(());
}
}
if self.running.is_some() {
let nested = transparent && spec.transparent && self.running.is_some() && spec.interactive.is_some();
if !nested && self.running.is_some() {
self.cancel();
}
(spec.enabled)(self).map_err(|m| EngineError::Disabled(spec.id.into(), m))?;
self.last_command = Some(spec.id.to_string());
self.echo(format!("Command: {}", spec.id.to_ascii_uppercase()));
if nested {
// The outer command stays the one Enter repeats.
self.echo(format!(">>{}", spec.id.to_ascii_uppercase()));
} else {
self.last_command = Some(spec.id.to_string());
self.echo(format!("Command: {}", spec.id.to_ascii_uppercase()));
}
match spec.interactive {
Some(factory) => {
let machine = guard::Guarded::create(spec.id, factory, self)?;
let st = self.state()?;
let before = st.doc.clone();
let selection_before = st.selection.clone();
if nested {
self.suspended = self.running.take();
}
self.running = Some(Running { id: spec.id.to_string(), machine, before, selection_before });
self.pending_window = None;
// Some commands complete immediately (e.g. ERASE with a pickfirst selection).
@@ -839,11 +856,13 @@ impl Session {
}
Ok(Step::Done) => {
self.finish(run, false);
self.resume_suspended();
Ok(())
}
Ok(Step::Cancel) => {
self.echo("*Cancel*");
self.finish(run, true);
self.resume_suspended();
Ok(())
}
Err(EngineError::Internal(id, m)) => {
@@ -852,6 +871,7 @@ impl Session {
st.doc = run.before.clone();
}
self.echo(format!("Internal error in {id}: {m}"));
self.resume_suspended();
Err(EngineError::Internal(id, m))
}
Err(e) => {
@@ -881,6 +901,15 @@ impl Session {
cmd::groups::after_command(self, Some(&run.before));
cmd::constraints::after_command(self, Some(&run.before), true);
assoc::after_command(self, Some(&run.before), None);
if self.suspended.is_some() {
// A transparent command's changes belong to the undo step of the command it ran inside.
if let Ok(st) = self.state_mut()
&& !Arc::ptr_eq(&run.before, &st.doc)
{
st.revision += 1;
}
return;
}
if let Ok(st) = self.state_mut()
&& !Arc::ptr_eq(&run.before, &st.doc)
{
@@ -890,10 +919,25 @@ impl Session {
}
}
/// Cancel the running command (Esc). With no command, clears the selection.
/// After a transparent command ends, the command it interrupted prompts again.
fn resume_suspended(&mut self) {
if let Some(outer) = self.suspended.take() {
self.echo(format!("Resuming {} command.", outer.id.to_ascii_uppercase()));
self.running = Some(outer);
}
}
/// Cancel the running command (Esc). With no command, clears the selection. Inside a
/// transparent command, cancels it and the command it interrupted.
pub fn cancel(&mut self) {
self.point_mods.clear();
if self.running.is_some() {
if self.suspended.is_some() {
if let Some(nested) = self.running.take() {
self.finish(nested, true);
}
self.running = self.suspended.take();
}
let _ = self.feed(Some(Input::Cancel));
} else {
self.pending_window = None;
@@ -918,6 +962,7 @@ impl Session {
/// The text shown on the command line: the active prompt, or "Command:".
pub fn prompt_text(&self) -> String {
match &self.running {
Some(r) if self.suspended.is_some() => format!(">>{} {}", r.id.to_ascii_uppercase(), r.machine.prompt(self).display()),
Some(r) => format!("{} {}", r.id.to_ascii_uppercase(), r.machine.prompt(self).display()),
None => "Command:".into(),
}
+49
View File
@@ -0,0 +1,49 @@
//! Transparent commands with their own prompts run nested inside the active command.
use cadcraft_engine::Session;
fn lines(s: &Session) -> usize {
s.doc().map(|d| d.entity_count()).unwrap_or(0)
}
#[test]
fn transparent_zoom_resumes_the_running_command() {
let mut s = Session::new();
s.cmdline("LINE 0,0").unwrap();
s.cmdline("'ZOOM").unwrap();
// The nested command prompts; the outer one is suspended, not cancelled.
assert_eq!(s.running.as_ref().map(|r| r.id.as_str()), Some("zoom"));
assert!(s.prompt_text().starts_with(">>"), "{}", s.prompt_text());
assert!(!s.log.iter().any(|l| l.contains("*Cancel*")), "{:?}", s.log);
s.cmdline("W").unwrap();
s.cmdline("-5,-5").unwrap();
s.cmdline("15,15").unwrap();
// ZOOM ended: LINE prompts again for the next point.
assert_eq!(s.running.as_ref().map(|r| r.id.as_str()), Some("line"), "{}", s.prompt_text());
assert!(!s.prompt_text().starts_with(">>"));
s.cmdline("10,0").unwrap();
s.cmdline("").unwrap();
assert!(s.running.is_none());
assert_eq!(lines(&s), 1);
// Enter repeats the outer command, not the transparent one.
assert_eq!(s.last_command.as_deref(), Some("line"));
s.cmdline("").unwrap();
s.cancel();
// Scripts nest the same way.
s.script("LINE 0,0 'PAN 0,0 1,1 5,0\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), 2);
// Enter ends the nested command; Esc and a new command cancel both levels.
s.cmdline("CIRCLE 0,0").unwrap();
s.cmdline("'PAN").unwrap();
s.cmdline("").unwrap();
assert_eq!(s.running.as_ref().map(|r| r.id.as_str()), Some("circle"));
s.cmdline("'ZOOM").unwrap();
s.cancel();
assert!(s.running.is_none() && s.suspended.is_none());
s.cmdline("CIRCLE 0,0").unwrap();
s.cmdline("'ZOOM").unwrap();
s.start("LINE").unwrap();
assert_eq!(s.running.as_ref().map(|r| r.id.as_str()), Some("line"));
assert!(s.suspended.is_none());
}