Skip to main content

quarb_session/
session.rs

1//! The session: a macro table where each accepted line becomes
2//! `def &N: <line> ;`, evaluated through an [`Executor`] and persisted
3//! through a [`Store`].
4//!
5//! History is the language's own reuse mechanism, not a bolted-on
6//! cell store: line 3 is the fragment `&3`, continued through the pipe
7//! (`&3 | /name::`, `&3 | [pred]`, `&3 @| count`). Frozen recall
8//! (`&N#`) replays a line's captured footprint; the live/version-
9//! pinned variants sharpen once a re-materializing executor (the
10//! daemon) is in play.
11
12use crate::{Cell, Executor, SessionState, Store};
13use anyhow::{Context, Result};
14use std::collections::HashMap;
15
16pub struct Session {
17    executor: Box<dyn Executor>,
18    store: Box<dyn Store>,
19    /// The macro table as definition text, re-parsed per query
20    /// (`def &1: …;\n …`). Kept as text so it seeds from `--defs`,
21    /// round-trips for display, and persists trivially.
22    defs_text: String,
23    /// Each line's rendered output, captured at commit — the frozen
24    /// footprint a `&N#` recall replays. In memory only for now.
25    snapshots: HashMap<usize, Vec<Cell>>,
26    /// The next line's number — the `&N` a fresh line will claim.
27    line_no: usize,
28}
29
30impl Session {
31    /// Build a session over an executor and a store, restoring any
32    /// persisted macro history from the store.
33    pub fn new(executor: Box<dyn Executor>, store: Box<dyn Store>) -> Session {
34        let state = store.load().unwrap_or_default();
35        let line_no = state.line_no.max(1);
36        Session {
37            executor,
38            store,
39            defs_text: state.defs_text,
40            snapshots: HashMap::new(),
41            line_no,
42        }
43    }
44
45    /// Swap the executor under a running session — an in-session
46    /// remount (`:mount`). History and the macro table stay: `&N`
47    /// re-runs against the new source set.
48    pub fn set_executor(&mut self, executor: Box<dyn Executor>) {
49        self.executor = executor;
50    }
51
52    /// Seed the macro table from a `--defs` file (validated first).
53    /// Stored stripped of `#` comment lines: the table is prepended
54    /// to query text, and the query lexer has no comment syntax.
55    pub fn seed_defs(&mut self, text: &str) -> Result<()> {
56        quarb::parse_defs(text).context("parsing --defs")?;
57        self.defs_text = format!("{}\n", quarb::strip_defs_comments(text));
58        self.persist();
59        Ok(())
60    }
61
62    /// Add a `def`/`macro` line to the table (validated first). Unlike
63    /// a query line, a definition is not run.
64    pub fn add_def(&mut self, line: &str) -> Result<()> {
65        let candidate = format!("{}{}\n", self.defs_text, line);
66        quarb::parse_defs(&candidate).context("parsing definition")?;
67        self.defs_text = candidate;
68        self.persist();
69        Ok(())
70    }
71
72    /// The line with the macro table prepended, so history refs
73    /// resolve inline.
74    fn combined(&self, line: &str) -> String {
75        if self.defs_text.is_empty() {
76            line.to_string()
77        } else {
78            format!("{}\n{line}", self.defs_text)
79        }
80    }
81
82    /// Evaluate a line against the standing arbor (`&N`). Pure —
83    /// history is not touched (a failed line commits nothing).
84    pub fn eval(&self, line: &str) -> Result<Vec<Cell>> {
85        self.executor.run(&self.combined(line))
86    }
87
88    /// The unresolved external references the last evaluation
89    /// recorded — see [`crate::Executor::refs`]. The host closes
90    /// the loop: acquire, remount, re-run.
91    pub fn refs(&self) -> Vec<String> {
92        self.executor.refs()
93    }
94
95    /// Evaluate a line against a freshly re-materialized source — the
96    /// `&N!` live reading, which sees current data.
97    pub fn eval_fresh(&self, line: &str) -> Result<Vec<Cell>> {
98        self.executor.run_fresh(&self.combined(line))
99    }
100
101    /// Run a line and render its results as exportable markup
102    /// (`md`, `html`, `txt`) — the panel's rendered-export path.
103    /// History refs resolve inline, exactly as in [`eval`](Self::eval);
104    /// an empty line exports the whole document.
105    pub fn export(&self, line: &str, kind: &str) -> Result<String> {
106        let line = line.trim();
107        if line.is_empty() {
108            return self.executor.export("", kind);
109        }
110        self.executor.export(&self.combined(line), kind)
111    }
112
113    /// Register an accepted line as `&N` and capture its output as the
114    /// frozen footprint for `&N#`. Returns whether the line's shape
115    /// could be a macro body (so `&N` will resolve); either way the
116    /// line number advances so labels track what the user saw.
117    pub fn commit(&mut self, line: &str, snapshot: Vec<Cell>) -> bool {
118        self.snapshots.insert(self.line_no, snapshot);
119        // A space before the `;` terminator: a line ending in a `::`
120        // projection would otherwise lex `::;` as the metadata sigil.
121        let candidate = format!("{}def &{}: {} ;\n", self.defs_text, self.line_no, line);
122        let referenceable = quarb::parse_defs(&candidate).is_ok();
123        if referenceable {
124            self.defs_text = candidate;
125        }
126        self.line_no += 1;
127        self.persist();
128        referenceable
129    }
130
131    /// The frozen output of line `n`, if captured — what a `&N#`
132    /// recall replays.
133    pub fn frozen(&self, n: usize) -> Option<&Vec<Cell>> {
134        self.snapshots.get(&n)
135    }
136
137    /// Record a frozen-recall line: it takes the next number and keeps
138    /// its own snapshot, but is not itself a referenceable macro body.
139    pub fn record_frozen(&mut self, snapshot: Vec<Cell>) {
140        self.snapshots.insert(self.line_no, snapshot);
141        self.line_no += 1;
142        self.persist();
143    }
144
145    /// Persist the durable state (best-effort; a store error does not
146    /// abort the session).
147    fn persist(&self) {
148        let state = SessionState {
149            defs_text: self.defs_text.clone(),
150            line_no: self.line_no,
151        };
152        let _ = self.store.save(&state);
153    }
154
155    /// The `&N` a fresh line will claim.
156    /// Tab-completion candidates for a line being edited.
157    /// Session-aware by construction: the executor answers with
158    /// its live arbor when it has one, the syntax tier otherwise.
159    pub fn complete(&self, text: &str, cursor: usize) -> Vec<quarb::complete::Candidate> {
160        self.executor.complete(text, cursor)
161    }
162
163    pub fn line_no(&self) -> usize {
164        self.line_no
165    }
166
167    /// The macro history text, for a `:history` command.
168    pub fn history(&self) -> &str {
169        &self.defs_text
170    }
171
172    /// Replace the macro history and line counter — restoring a
173    /// persisted session (e.g. from the browser's localStorage). Frozen
174    /// snapshots are not restored; they regenerate on re-run.
175    pub fn restore(&mut self, defs_text: String, line_no: usize) {
176        self.defs_text = defs_text;
177        self.line_no = line_no.max(1);
178    }
179
180    /// Clear the macro history and restart line numbering.
181    pub fn reset(&mut self) {
182        self.defs_text.clear();
183        self.snapshots.clear();
184        self.line_no = 1;
185        self.persist();
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::MemStore;
193
194    struct NullExec;
195    impl Executor for NullExec {
196        fn run(&self, _query: &str) -> anyhow::Result<Vec<Cell>> {
197            Ok(vec![])
198        }
199    }
200
201    /// A commented defs file seeds a table the query lexer can take:
202    /// the stored text is prepended to every line, and the lexer has
203    /// no comment syntax, so `#` lines must not survive seeding.
204    #[test]
205    fn seed_defs_strips_comments() {
206        let mut s = Session::new(Box::new(NullExec), Box::new(MemStore));
207        s.seed_defs("# a library header\ndef &a: /x;\n# and a note\ndef &b: /y;")
208            .unwrap();
209        assert!(!s.history().contains('#'), "history: {}", s.history());
210        assert!(s.history().contains("def &a"));
211        assert!(s.history().contains("def &b"));
212    }
213}