mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-01 06:46:50 -06:00
644 lines
25 KiB
Rust
644 lines
25 KiB
Rust
//! AutoLISP: load `.lsp` files, evaluate expressions typed at the command line, and run
|
|
//! commands defined with `(defun c:NAME …)`.
|
|
//!
|
|
//! The language runs on an explicit-stack machine ([`eval`]) that can stop for input
|
|
//! (`getpoint`, `pause`) and carry on later, so the interface keeps drawing while a routine
|
|
//! waits. Each drawing has its own symbols, as in AutoCAD. A run is one undo step.
|
|
|
|
pub mod builtins;
|
|
mod drawing;
|
|
pub mod eval;
|
|
pub mod input;
|
|
pub mod read;
|
|
mod startup;
|
|
pub mod value;
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use cadcraft_doc::{Drawing, Handle};
|
|
|
|
use crate::{Input, Prompt, Session};
|
|
use eval::{Ctx, Machine, Need, Outcome};
|
|
use value::{Sym, Value};
|
|
|
|
/// A LISP error: the message `*error*` receives (`bad argument type: numberp: nil`…).
|
|
#[derive(Clone, Debug)]
|
|
pub struct LispError {
|
|
pub msg: String,
|
|
/// `exit`/`quit`, or a cancelled input: not caught by `vl-catch-all-apply`.
|
|
pub quit: bool,
|
|
}
|
|
|
|
impl LispError {
|
|
pub fn new(msg: impl Into<String>) -> LispError {
|
|
LispError { msg: msg.into(), quit: false }
|
|
}
|
|
pub fn bad_arg(pred: &str, v: &Value) -> LispError {
|
|
LispError::new(format!("bad argument type: {pred}: {}", value::to_text(v, true)))
|
|
}
|
|
pub fn quit(msg: impl Into<String>) -> LispError {
|
|
LispError { msg: msg.into(), quit: true }
|
|
}
|
|
}
|
|
|
|
/// One drawing's LISP environment.
|
|
pub struct Env {
|
|
pub globals: HashMap<Sym, Value>,
|
|
/// Printed text not yet ended by a newline.
|
|
pub out: String,
|
|
/// `initget` settings for the next `get…` call.
|
|
pub initget: Option<input::Initget>,
|
|
pub next_pickset: u32,
|
|
pub files: HashMap<u32, builtins::LispFile>,
|
|
pub next_file: u32,
|
|
/// Entities `entdel` deleted (it brings them back when called again), with their space.
|
|
pub deleted: HashMap<Handle, (cadcraft_doc::Space, Arc<cadcraft_doc::Entity>)>,
|
|
/// Every entity's handle in creation order, for the drawing it was taken from (`entnext`).
|
|
pub order: Option<(usize, Arc<Vec<Handle>>)>,
|
|
/// `tblnext`'s place in each table.
|
|
pub tblnext: HashMap<String, usize>,
|
|
/// A block definition `entmake` has begun (a BLOCK list) and not yet ended (ENDBLK): the
|
|
/// entities made meanwhile go into it, numbered when it ends.
|
|
pub pending_block: Option<(cadcraft_doc::Block, Vec<cadcraft_doc::Entity>)>,
|
|
/// The command history's length after LISP last printed a line: when something else wrote since
|
|
/// (a prompt, a command's echo), a leading newline only ends that line.
|
|
pub printed_at: Option<usize>,
|
|
}
|
|
|
|
impl Default for Env {
|
|
fn default() -> Self {
|
|
let mut e = Env {
|
|
globals: HashMap::new(),
|
|
out: String::new(),
|
|
initget: None,
|
|
next_pickset: 1,
|
|
files: HashMap::new(),
|
|
next_file: 1,
|
|
deleted: HashMap::new(),
|
|
order: None,
|
|
tblnext: HashMap::new(),
|
|
pending_block: None,
|
|
printed_at: None,
|
|
};
|
|
builtins::install(&mut e);
|
|
e
|
|
}
|
|
}
|
|
|
|
impl Env {
|
|
/// Set a variable (nil unbinds it).
|
|
pub fn set(&mut self, s: &Sym, v: Value) {
|
|
if v.is_nil() {
|
|
self.globals.remove(s);
|
|
} else {
|
|
self.globals.insert(s.clone(), v);
|
|
}
|
|
}
|
|
pub fn get(&self, name: &str) -> Value {
|
|
eval::lookup(self, &Sym::new(name))
|
|
}
|
|
/// Text printed by `princ` and friends.
|
|
pub fn print(&mut self, text: &str) {
|
|
self.out.push_str(text);
|
|
}
|
|
}
|
|
|
|
/// The session's LISP: each drawing's environment and the routine running, if any.
|
|
#[derive(Default)]
|
|
pub struct LispState {
|
|
envs: HashMap<u64, Env>,
|
|
run: Option<Box<Run>>,
|
|
/// A routine runs, from its start to its end (also while its machine is out of `run`).
|
|
active: bool,
|
|
/// Files loaded by APPLOAD, by name, for `(load name)` where there is no file system.
|
|
files: HashMap<String, String>,
|
|
/// Where startup files are read from (see `startup.rs`).
|
|
support_dir: Option<std::path::PathBuf>,
|
|
/// Drawings that had their startup files.
|
|
autoloaded: std::collections::HashSet<u64>,
|
|
/// `acad.lsp` was loaded this session.
|
|
acad_loaded: bool,
|
|
/// Memory caps (lowered by tests).
|
|
pub(crate) limits: Limits,
|
|
}
|
|
|
|
/// What a run may use (see [`value`]'s memory notes).
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(crate) struct Limits {
|
|
/// Live cons cells (all sessions).
|
|
pub cells: usize,
|
|
/// Bytes of strings, printed text and files read per run.
|
|
pub string_bytes: u64,
|
|
/// Maximum interpreter steps per synchronous callback.
|
|
pub callback_steps: u64,
|
|
}
|
|
|
|
impl Default for Limits {
|
|
/// Provide finite cons-cell, string-byte, and synchronous-callback limits for a LISP run.
|
|
fn default() -> Self {
|
|
Limits { cells: value::MAX_CELLS, string_bytes: value::STRING_BUDGET, callback_steps: 500_000_000 }
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for LispState {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "LispState {{ drawings: {}, running: {} }}", self.envs.len(), self.run.is_some())
|
|
}
|
|
}
|
|
|
|
/// A routine on its way: the machine, what it waits for, and its undo step.
|
|
struct Run {
|
|
machine: Machine,
|
|
/// The drawing it runs in.
|
|
uid: u64,
|
|
waiting: Option<Need>,
|
|
/// A command-line expression: its value is printed at the end.
|
|
print: bool,
|
|
/// Undo label: the command (`c:` function) or "LISP".
|
|
label: String,
|
|
/// The drawing and selection before the run (one undo step for the whole run).
|
|
before: Option<(Arc<Drawing>, Vec<Handle>)>,
|
|
/// Inputs given up front (`lisp {code, inputs}`): what `get…` calls answer.
|
|
inputs: std::collections::VecDeque<Value>,
|
|
/// Run to the end without waiting (programmatic calls): `get…` without an input gets nil.
|
|
blocking: bool,
|
|
/// Steps taken, for the cap on blocking runs.
|
|
steps: u64,
|
|
/// The `*error*` handler is the one running.
|
|
handler: bool,
|
|
/// Bytes of strings and printed text the run has made ([`value::charge_bytes`]).
|
|
string_bytes: u64,
|
|
/// Cons cells alive when the run started: cells the drawing's variables already hold don't
|
|
/// stop a run (it can still set them to nil) unless it adds more.
|
|
cells_at_start: usize,
|
|
}
|
|
|
|
/// Steps per slice before the interface gets a frame.
|
|
const SLICE: u64 = 400_000;
|
|
/// Steps a blocking run (a programmatic call) may take before it is stopped.
|
|
const BLOCKING_CAP: u64 = 500_000_000;
|
|
|
|
impl Session {
|
|
/// The LISP environment of the active drawing.
|
|
fn lisp_env_take(&mut self) -> Env {
|
|
let uid = self.state().map(|s| s.uid).unwrap_or(0);
|
|
self.lisp.envs.remove(&uid).unwrap_or_default()
|
|
}
|
|
|
|
fn lisp_env_put(&mut self, uid: u64, env: Env) {
|
|
self.lisp.envs.insert(uid, env);
|
|
}
|
|
|
|
/// A routine is running (possibly waiting for input).
|
|
pub fn lisp_running(&self) -> bool {
|
|
self.lisp.active
|
|
}
|
|
|
|
/// A prompt waits for input: a command's, or a LISP routine's `get…` function. Clicks and
|
|
/// typing answer it; with none, the interface is idle (clicks select, Delete erases).
|
|
pub fn prompting(&self) -> bool {
|
|
self.running.is_some() || self.lisp_prompt().is_some()
|
|
}
|
|
|
|
/// A routine is stopped, waiting for input or for a frame.
|
|
pub(crate) fn lisp_waiting(&self) -> bool {
|
|
self.lisp.run.is_some()
|
|
}
|
|
|
|
/// The value of a LISP variable in the active drawing (nil when unbound).
|
|
pub fn lisp_var(&self, name: &str) -> Value {
|
|
let uid = self.state().map(|s| s.uid).unwrap_or(0);
|
|
self.lisp.envs.get(&uid).map(|e| e.get(name)).unwrap_or_default()
|
|
}
|
|
|
|
/// Evaluate LISP source (a command-line expression or a file's contents). `print`: show the
|
|
/// value on the command line. `blocking`: run to the end, answering `get…` calls from
|
|
/// `inputs` (nil when they run out), as programmatic calls must; otherwise the routine may
|
|
/// wait for the user.
|
|
pub fn lisp_eval(
|
|
&mut self,
|
|
src: &str,
|
|
print: bool,
|
|
blocking: bool,
|
|
inputs: Vec<Value>,
|
|
) -> crate::Result<Option<std::result::Result<Value, String>>> {
|
|
let forms = read::read_all(src).map_err(|e| crate::EngineError::Other(format!("; error: {}", e.msg)))?;
|
|
self.lisp_start(Machine::forms(forms), print, blocking, inputs, "LISP".into())
|
|
}
|
|
|
|
/// Call a LISP function (`c:` commands typed at the command line).
|
|
fn lisp_call(&mut self, f: Value, label: String) -> crate::Result<Option<std::result::Result<Value, String>>> {
|
|
self.lisp_start(Machine::calling(f, Vec::new()), false, false, Vec::new(), label)
|
|
}
|
|
|
|
fn lisp_start(
|
|
&mut self,
|
|
machine: Machine,
|
|
print: bool,
|
|
blocking: bool,
|
|
inputs: Vec<Value>,
|
|
label: String,
|
|
) -> crate::Result<Option<std::result::Result<Value, String>>> {
|
|
if self.lisp.active {
|
|
return Err(crate::EngineError::Other("a LISP routine is already running".into()));
|
|
}
|
|
self.lisp.active = true;
|
|
let st = self.state()?;
|
|
let before = Some((st.doc.clone(), st.selection.clone()));
|
|
let uid = st.uid;
|
|
self.lisp.run = Some(Box::new(Run {
|
|
machine,
|
|
uid,
|
|
waiting: None,
|
|
print,
|
|
label,
|
|
before,
|
|
inputs: inputs.into(),
|
|
blocking,
|
|
steps: 0,
|
|
handler: false,
|
|
string_bytes: 0,
|
|
cells_at_start: value::live_cells(),
|
|
}));
|
|
self.lisp_drive();
|
|
if blocking {
|
|
// A blocking run that waits for a command's input (`pause`) can't get it: cancel.
|
|
if self.lisp.run.is_some() {
|
|
self.lisp_abort("Function cancelled");
|
|
}
|
|
}
|
|
Ok(self.lisp_last.take())
|
|
}
|
|
|
|
/// Call once per frame: loads the startup files into a drawing seen for the first time, and
|
|
/// carries on a routine that gave the interface a frame.
|
|
pub fn lisp_tick(&mut self) {
|
|
// Called every frame, outside any command: a panic in it ends the routine with an error
|
|
// instead of taking down the app.
|
|
if let Err(p) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.lisp_tick_unguarded())) {
|
|
self.lisp_internal_error(&crate::guard::message(&*p));
|
|
}
|
|
}
|
|
|
|
fn lisp_tick_unguarded(&mut self) {
|
|
// A drawing seen for the first time gets its startup files.
|
|
self.lisp_autoload();
|
|
if self.lisp.run.as_ref().is_some_and(|r| matches!(r.waiting, Some(Need::Yield))) {
|
|
if let Some(r) = self.lisp.run.as_mut() {
|
|
r.waiting = None;
|
|
}
|
|
self.lisp_drive();
|
|
}
|
|
}
|
|
|
|
/// Whether the routine needs ticks ([`Session::lisp_tick`]) to go on.
|
|
pub fn lisp_busy(&self) -> bool {
|
|
self.lisp.run.as_ref().is_some_and(|r| matches!(r.waiting, Some(Need::Yield)))
|
|
}
|
|
|
|
/// Run the machine until it finishes or waits.
|
|
fn lisp_drive(&mut self) {
|
|
loop {
|
|
let Some(mut run) = self.lisp.run.take() else { return };
|
|
if run.waiting.is_some() {
|
|
self.lisp.run = Some(run);
|
|
return;
|
|
}
|
|
// The routine belongs to its drawing; another one became active: stop it.
|
|
if self.state().map(|s| s.uid).ok() != Some(run.uid) {
|
|
self.lisp.run = Some(run);
|
|
self.lisp_abort("Function cancelled");
|
|
return;
|
|
}
|
|
let mut env = self.lisp_env_take();
|
|
value::set_string_budget(run.string_bytes, self.lisp.limits.string_bytes);
|
|
value::set_cell_limit(self.lisp.limits.cells.max(run.cells_at_start));
|
|
let outcome = {
|
|
let machine = &mut run.machine;
|
|
let mut ctx = Ctx { s: self, env: &mut env, depth: 0 };
|
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| machine.run(&mut ctx, SLICE)))
|
|
};
|
|
run.string_bytes = value::string_bytes_used();
|
|
let outcome = match outcome {
|
|
Ok(o) => o,
|
|
Err(p) => {
|
|
// A panic inside a built-in: put the drawing back as the routine found it.
|
|
if let Some((doc, sel)) = &run.before
|
|
&& let Some(st) = self.docs.iter_mut().find(|d| d.uid == run.uid)
|
|
{
|
|
st.doc = doc.clone();
|
|
st.selection = sel.clone();
|
|
}
|
|
run.handler = true;
|
|
Outcome::Error(LispError::quit(format!("internal error: {}", crate::guard::message(&*p))))
|
|
}
|
|
};
|
|
let uid = run.uid;
|
|
self.lisp_flush(&mut env, false);
|
|
self.lisp_env_put(uid, env);
|
|
match outcome {
|
|
Outcome::Done(v) => {
|
|
self.lisp.run = Some(run);
|
|
self.lisp_end(Ok(v));
|
|
return;
|
|
}
|
|
Outcome::Error(e) => {
|
|
self.lisp.run = Some(run);
|
|
self.lisp_error(e);
|
|
return;
|
|
}
|
|
Outcome::Wait(Need::Yield) => {
|
|
run.steps = run.steps.saturating_add(SLICE);
|
|
if run.blocking {
|
|
if run.steps > BLOCKING_CAP {
|
|
self.lisp.run = Some(run);
|
|
self.lisp_error(LispError::quit(format!("routine stopped after {BLOCKING_CAP} steps")));
|
|
return;
|
|
}
|
|
self.lisp.run = Some(run);
|
|
continue;
|
|
}
|
|
run.waiting = Some(Need::Yield);
|
|
self.lisp.run = Some(run);
|
|
return;
|
|
}
|
|
Outcome::Wait(Need::Pause) => {
|
|
// The command `command` left running takes the next input; with none
|
|
// running there is nothing to wait for.
|
|
if self.running.is_none() || run.blocking {
|
|
run.machine.resume(Value::Nil);
|
|
if run.blocking && self.running.is_some() {
|
|
self.cancel();
|
|
}
|
|
self.lisp.run = Some(run);
|
|
continue;
|
|
}
|
|
run.waiting = Some(Need::Pause);
|
|
self.lisp.run = Some(run);
|
|
return;
|
|
}
|
|
Outcome::Wait(Need::Get(req)) => {
|
|
if let Some(v) = run.inputs.pop_front() {
|
|
run.machine.resume(v);
|
|
self.lisp.run = Some(run);
|
|
continue;
|
|
}
|
|
if run.blocking {
|
|
run.machine.resume(Value::Nil);
|
|
self.lisp.run = Some(run);
|
|
continue;
|
|
}
|
|
run.waiting = Some(Need::Get(req));
|
|
self.lisp.run = Some(run);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Move printed text to the command history: complete lines, and with `all` the rest too.
|
|
fn lisp_flush(&mut self, env: &mut Env, all: bool) {
|
|
while let Some(i) = env.out.find('\n') {
|
|
let line: String = env.out.drain(..=i).collect();
|
|
let line = line.trim_end_matches(['\n', '\r']).to_string();
|
|
// `(princ "\nText")` after a prompt or a command's echo ends that line instead of
|
|
// leaving an empty one; after LISP's own line it does print an empty line.
|
|
if line.is_empty() && env.printed_at != Some(self.log.len()) {
|
|
env.printed_at = Some(self.log.len());
|
|
continue;
|
|
}
|
|
self.echo(line);
|
|
env.printed_at = Some(self.log.len());
|
|
}
|
|
if all && !env.out.is_empty() {
|
|
let line = std::mem::take(&mut env.out);
|
|
self.echo(line);
|
|
env.printed_at = Some(self.log.len());
|
|
}
|
|
}
|
|
|
|
/// The routine finished: print its value (command-line expressions), record its undo step.
|
|
fn lisp_end(&mut self, result: std::result::Result<Value, String>) {
|
|
let Some(run) = self.lisp.run.take() else { return };
|
|
self.lisp.active = false;
|
|
let mut env = self.lisp.envs.remove(&run.uid).unwrap_or_default();
|
|
self.lisp_flush(&mut env, true);
|
|
self.lisp.envs.insert(run.uid, env);
|
|
if let (Ok(v), true) = (&result, run.print) {
|
|
let text = value::to_text(v, true);
|
|
if !builtins::is_null_symbol(v) {
|
|
self.echo(text);
|
|
}
|
|
}
|
|
if let Some((doc, sel)) = run.before
|
|
&& let Some(st) = self.docs.iter_mut().find(|d| d.uid == run.uid)
|
|
&& !Arc::ptr_eq(&doc, &st.doc)
|
|
{
|
|
st.push_undo(crate::Snapshot { label: run.label.clone(), doc, selection: sel });
|
|
st.revision += 1;
|
|
}
|
|
self.lisp_last = Some(result);
|
|
}
|
|
|
|
/// A LISP error: run `*error*` when the routine defined one, else print it.
|
|
fn lisp_error(&mut self, e: LispError) {
|
|
let Some(mut run) = self.lisp.run.take() else { return };
|
|
// A block definition the failed routine began is dropped: a later routine's entities
|
|
// would otherwise go into it instead of the drawing (#1662). A definition still spans
|
|
// runs that end normally.
|
|
if let Some(env) = self.lisp.envs.get_mut(&run.uid) {
|
|
env.pending_block = None;
|
|
}
|
|
// The command a failed routine left running is cancelled.
|
|
if self.running.is_some() {
|
|
self.cancel();
|
|
}
|
|
let handler = self.lisp.envs.get(&run.uid).map(|env| env.get("*ERROR*")).unwrap_or_default();
|
|
if !run.handler && matches!(handler, Value::Func(_) | Value::Subr(_)) {
|
|
run.handler = true;
|
|
run.waiting = None;
|
|
run.print = false;
|
|
run.machine = Machine::calling(handler, vec![Value::str(&e.msg)]);
|
|
self.lisp.run = Some(run);
|
|
self.lisp_drive();
|
|
// The routine failed whatever its handler returned.
|
|
if let Some(Ok(_)) = &self.lisp_last {
|
|
self.lisp_last = Some(Err(e.msg));
|
|
}
|
|
return;
|
|
}
|
|
self.lisp.run = Some(run);
|
|
let mut env = self.lisp_env_take();
|
|
self.lisp_flush(&mut env, true);
|
|
let uid = self.state().map(|s| s.uid).unwrap_or(0);
|
|
self.lisp_env_put(uid, env);
|
|
self.echo(format!("; error: {}", e.msg));
|
|
self.lisp_end(Err(e.msg));
|
|
}
|
|
|
|
/// A panic escaped the LISP machinery outside a routine's steps: end the routine, if any,
|
|
/// with an error, so the session stays usable.
|
|
fn lisp_internal_error(&mut self, msg: &str) {
|
|
let msg = format!("internal error: {msg}");
|
|
if self.lisp.run.is_some() {
|
|
if let Some(r) = self.lisp.run.as_mut() {
|
|
// Don't run the routine's own `*error*` after an internal failure.
|
|
r.handler = true;
|
|
}
|
|
self.lisp_error(LispError::quit(msg));
|
|
} else if self.lisp.active {
|
|
self.lisp.active = false;
|
|
self.echo(format!("; error: {msg}"));
|
|
self.lisp_last = Some(Err(msg));
|
|
} else {
|
|
self.echo(format!("; error: {msg}"));
|
|
}
|
|
}
|
|
|
|
/// Stop the routine (Esc while it waits, its drawing closed) with `msg` as its error.
|
|
pub fn lisp_abort(&mut self, msg: &str) {
|
|
if self.lisp.run.is_some() {
|
|
self.lisp_error(LispError::quit(msg));
|
|
}
|
|
}
|
|
|
|
/// A routine waits for the user's answer to a `get…` function: its prompt.
|
|
pub(crate) fn lisp_prompt(&self) -> Option<Prompt> {
|
|
match self.lisp.run.as_ref()?.waiting.as_ref()? {
|
|
Need::Get(req) => Some(req.prompt()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// The rubber band of a waiting `get…` function: a rectangle from `getcorner`'s corner, a
|
|
/// line from the base point of the others. `None` when no routine waits for input.
|
|
pub(crate) fn lisp_preview(&self, cursor: cadcraft_geom::Vec2) -> Option<Vec<cadcraft_doc::EntityKind>> {
|
|
let Need::Get(req) = self.lisp.run.as_ref()?.waiting.as_ref()? else { return None };
|
|
let line =
|
|
|a: cadcraft_geom::Vec2, b: cadcraft_geom::Vec2| cadcraft_doc::EntityKind::Line(cadcraft_doc::Line { a: a.to3(0.0), b: b.to3(0.0) });
|
|
let Some(b) = req.first.or(req.base).map(|p| cadcraft_geom::Vec2::new(p[0], p[1])) else { return Some(Vec::new()) };
|
|
Some(match req.kind {
|
|
input::GetKind::Corner => {
|
|
let (c1, c2) = (cadcraft_geom::Vec2::new(cursor.x, b.y), cadcraft_geom::Vec2::new(b.x, cursor.y));
|
|
vec![line(b, c1), line(c1, cursor), line(cursor, c2), line(c2, b)]
|
|
}
|
|
input::GetKind::Point | input::GetKind::Dist | input::GetKind::Angle { .. } => vec![line(b, cursor)],
|
|
_ => Vec::new(),
|
|
})
|
|
}
|
|
|
|
/// Give an input to a waiting routine. `true` when the routine took it.
|
|
pub(crate) fn lisp_input(&mut self, input: Input) -> bool {
|
|
let Some(req) = self.lisp.run.as_ref().and_then(|r| match &r.waiting {
|
|
Some(Need::Get(req)) => Some(req.clone()),
|
|
_ => None,
|
|
}) else {
|
|
return false;
|
|
};
|
|
if input == Input::Cancel {
|
|
self.echo("*Cancel*");
|
|
self.lisp_abort("Function cancelled");
|
|
return true;
|
|
}
|
|
match req.answer(self, input) {
|
|
input::Answer::Value(v) => {
|
|
if let Some(r) = self.lisp.run.as_mut() {
|
|
r.waiting = None;
|
|
r.machine.resume(v);
|
|
}
|
|
self.lisp_drive();
|
|
}
|
|
// Not an answer this prompt takes: say why and ask again.
|
|
input::Answer::Again(msg) => self.echo(msg),
|
|
// The first of two points: the prompt asks for the second.
|
|
input::Answer::Second(p) => {
|
|
if let Some(r) = self.lisp.run.as_mut() {
|
|
r.waiting = Some(Need::Get(input::with_first(&req, p)));
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/// After the running command took an input: a routine paused for it carries on.
|
|
pub(crate) fn lisp_after_command_input(&mut self) {
|
|
if self.lisp.run.as_ref().is_some_and(|r| matches!(r.waiting, Some(Need::Pause))) {
|
|
if let Some(r) = self.lisp.run.as_mut() {
|
|
r.waiting = None;
|
|
r.machine.resume(Value::Nil);
|
|
}
|
|
self.lisp_drive();
|
|
}
|
|
}
|
|
|
|
/// Whether commands' undo steps go into a running routine's (one step per routine).
|
|
pub(crate) fn lisp_groups_undo(&self) -> bool {
|
|
self.lisp.active
|
|
}
|
|
|
|
/// The commands the active drawing's LISP defines (`c:` functions), uppercase and sorted.
|
|
pub fn lisp_commands(&self) -> Vec<String> {
|
|
let Ok(uid) = self.state().map(|s| s.uid) else { return Vec::new() };
|
|
let Some(env) = self.lisp.envs.get(&uid) else { return Vec::new() };
|
|
let mut names: Vec<String> = env
|
|
.globals
|
|
.iter()
|
|
.filter(|(name, f)| name.name().starts_with("C:") && matches!(f, Value::Func(_) | Value::Subr(_)))
|
|
.filter_map(|(name, _)| name.name().strip_prefix("C:").filter(|n| !n.is_empty()).map(str::to_string))
|
|
.collect();
|
|
names.sort();
|
|
names
|
|
}
|
|
|
|
/// A `c:` function for a command name typed at the command line.
|
|
pub(crate) fn lisp_command(&self, name: &str) -> Option<Value> {
|
|
if self.lisp.active {
|
|
return None;
|
|
}
|
|
let uid = self.state().ok()?.uid;
|
|
let f = self.lisp.envs.get(&uid)?.get(&format!("C:{name}"));
|
|
matches!(f, Value::Func(_) | Value::Subr(_)).then_some(f)
|
|
}
|
|
|
|
/// Run a `c:` command.
|
|
pub(crate) fn lisp_run_command(&mut self, name: &str, f: Value) -> crate::Result<()> {
|
|
self.lisp_call(f, name.to_ascii_uppercase()).map(|_| ())
|
|
}
|
|
}
|
|
|
|
pub(crate) use drawing::from_json as drawing_from_json;
|
|
pub(crate) use drawing::load_source;
|
|
|
|
impl Session {
|
|
/// A file APPLOAD loaded, by its name (with or without `.lsp`, any case).
|
|
pub(crate) fn lisp_file(&self, name: &str) -> Option<String> {
|
|
let key = file_key(name);
|
|
self.lisp.files.get(&key).cloned()
|
|
}
|
|
|
|
/// Load a LISP file (APPLOAD, a dropped `.lsp` file): its forms run in the active drawing,
|
|
/// and `(load name)` finds it later even where there is no file system. `blocking`: run to
|
|
/// the end (programmatic calls).
|
|
pub fn lisp_load(&mut self, name: &str, text: &str, blocking: bool) -> crate::Result<Option<std::result::Result<Value, String>>> {
|
|
self.lisp.files.insert(file_key(name), text.to_string());
|
|
let r = self.lisp_eval(text, false, blocking, Vec::new())?;
|
|
if !matches!(r, Some(Err(_))) {
|
|
let shown = std::path::Path::new(name).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(name).to_string();
|
|
self.echo(format!("{shown} successfully loaded."));
|
|
}
|
|
Ok(r)
|
|
}
|
|
}
|
|
|
|
fn file_key(name: &str) -> String {
|
|
let base = std::path::Path::new(&name.replace('\\', "/")).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(name).to_ascii_lowercase();
|
|
base.strip_suffix(".lsp").map(str::to_string).unwrap_or(base)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
#[cfg(test)]
|
|
mod block_tests;
|