Skip to main content

repl_lang/
session.rs

1//! Session state: pending input, committed entries, and the feed loop.
2
3use alloc::{string::String, vec::Vec};
4
5use source_lang::{SourceFile, SourceId, SourceMap, Span};
6
7use crate::{Entry, Feed, Input, SessionError, Status};
8
9/// The state of one REPL session: the entry being typed, and every entry
10/// accepted so far.
11///
12/// A REPL reads a line, decides whether it finishes a piece of code, and if
13/// not keeps reading. `Session` owns that decision loop without owning the
14/// language. Each call to [`feed`](Session::feed) appends a line to the
15/// pending entry and runs the caller's *pipeline* — lex, parse, evaluate,
16/// whatever the language does — on all of it. The pipeline answers
17/// [`Status::Complete`] with its result, or [`Status::Incomplete`] to ask for
18/// another line.
19///
20/// A completed entry is committed to the session's [`SourceMap`] as a source
21/// named `<repl:N>`, laid out after the entries before it in one global
22/// position space. The pipeline sees that position before the entry is
23/// committed (as [`Input::base`]), so the spans it records are final the
24/// moment they are made. A diagnostic produced by entry 7 can point at a
25/// definition in entry 2, and a renderer given [`sources`](Session::sources)
26/// shows both lines with the right names.
27///
28/// The session does no I/O. Pair it with any line source: an [`Editor`]
29/// driven by a terminal, `stdin` read line by line, a script, a test.
30///
31/// [`Editor`]: crate::Editor
32///
33/// # Cost
34///
35/// While an entry is incomplete the pipeline re-reads it in full for every
36/// new line, so an entry of *k* lines is processed *k* times. Interactive
37/// entries are short and this keeps the pipeline stateless. A host that
38/// receives a large paste should feed it as one chunk: `feed` accepts text
39/// containing newlines. [`with_limit`](Session::with_limit) caps entry size.
40///
41/// The pending buffer is reused across entries. Committing an entry makes one
42/// exact-size copy of its text and its name into the source map; otherwise
43/// feeding allocates only when the pending buffer grows past its previous
44/// size.
45///
46/// # Examples
47///
48/// A REPL loop over a fixed script, with a pipeline that waits for balanced
49/// parentheses and then reports how many lines the entry spanned:
50///
51/// ```
52/// use repl_lang::{Feed, Session, Status};
53///
54/// let mut session = Session::new();
55/// let mut results = Vec::new();
56///
57/// for line in ["(define (square x)", "  (* x x))", "", "(square 4)"] {
58///     let feed = session.feed(line, |input| {
59///         let depth: i32 = input.text().chars().map(|c| match c {
60///             '(' => 1,
61///             ')' => -1,
62///             _ => 0,
63///         }).sum();
64///         if depth > 0 { Status::Incomplete } else { Status::Complete(input.text().lines().count()) }
65///     })?;
66///     if let Feed::Complete { entry, value } = feed {
67///         results.push((entry.number(), value));
68///     }
69/// }
70///
71/// assert_eq!(results, [(1, 2), (2, 1)]);
72/// assert_eq!(session.len(), 2);
73/// assert_eq!(session.entry(1).map(|e| e.text()), Some("(define (square x)\n  (* x x))\n"));
74/// # Ok::<(), repl_lang::SessionError>(())
75/// ```
76#[derive(Clone, Debug)]
77pub struct Session {
78    /// One source per committed entry, in entry order.
79    sources: SourceMap,
80    /// The id of entry `n` is `ids[n - 1]`.
81    ids: Vec<SourceId>,
82    /// The entry being accumulated; every line ends with `'\n'`.
83    pending: String,
84    /// Reused buffer for building entry names.
85    name: String,
86    /// The global offset the next committed entry will start at.
87    next_base: u32,
88    /// Maximum byte length of one entry.
89    limit: usize,
90}
91
92impl Default for Session {
93    #[inline]
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl Session {
100    /// The entry size limit of [`Session::new`]: 1 MiB.
101    pub const DEFAULT_LIMIT: usize = 1 << 20;
102
103    /// Creates a session with no entries and the
104    /// [`DEFAULT_LIMIT`](Session::DEFAULT_LIMIT) on entry size.
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use repl_lang::Session;
110    ///
111    /// let session = Session::new();
112    /// assert!(session.is_empty());
113    /// assert!(!session.is_pending());
114    /// assert_eq!(session.limit(), Session::DEFAULT_LIMIT);
115    /// ```
116    #[must_use]
117    pub fn new() -> Self {
118        Self::with_limit(Self::DEFAULT_LIMIT)
119    }
120
121    /// Creates a session whose entries may grow to at most `limit` bytes,
122    /// counting the `'\n'` that ends each line.
123    ///
124    /// A line that would take the pending entry past the limit is refused with
125    /// [`SessionError::TooLong`], and the entry is discarded. The limit bounds
126    /// memory held for one entry and the work the pipeline repeats on each
127    /// continuation line, so lower it when input is untrusted.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use repl_lang::{Feed, Session, SessionError, Status};
133    ///
134    /// let mut session = Session::with_limit(16);
135    /// let more = |_: repl_lang::Input<'_>| Status::<()>::Incomplete;
136    ///
137    /// assert!(matches!(session.feed("0123456789", more)?, Feed::Incomplete)); // 11 bytes
138    /// let err = session.feed("abcdef", more).unwrap_err(); // would be 18
139    /// assert_eq!(err, SessionError::TooLong { len: 18, limit: 16 });
140    /// assert!(!session.is_pending());
141    /// # Ok::<(), SessionError>(())
142    /// ```
143    #[must_use]
144    pub fn with_limit(limit: usize) -> Self {
145        Self {
146            sources: SourceMap::new(),
147            ids: Vec::new(),
148            pending: String::new(),
149            name: String::new(),
150            next_base: 0,
151            limit,
152        }
153    }
154
155    /// Appends a line to the pending entry and runs `pipeline` on the whole
156    /// entry.
157    ///
158    /// `line` is one line of input, with or without its line terminator; a
159    /// single trailing `"\n"` or `"\r\n"` is removed and a `'\n'` appended, so
160    /// lines from `read_line` and from `str::lines` behave the same. It may
161    /// also be several lines at once, as from a paste.
162    ///
163    /// What happens next:
164    ///
165    /// - If no entry is pending and `line` is blank, nothing is evaluated:
166    ///   the result is [`Feed::Empty`] and the pipeline is not called.
167    /// - Otherwise the line joins the pending entry and the pipeline is called
168    ///   with an [`Input`] over all of it.
169    /// - On [`Status::Incomplete`] the entry stays pending and the result is
170    ///   [`Feed::Incomplete`]. Blank lines inside an entry are passed through,
171    ///   so a language can use one to end a block.
172    /// - On [`Status::Complete`] the entry is committed as `<repl:N>` and the
173    ///   result is [`Feed::Complete`] with the pipeline's value. The pending
174    ///   buffer is cleared for the next entry.
175    ///
176    /// # Errors
177    ///
178    /// - [`SessionError::TooLong`] if the entry would exceed
179    ///   [`limit`](Session::limit).
180    /// - [`SessionError::SpaceExhausted`] if the session's source map cannot
181    ///   hold the entry.
182    ///
183    /// In both cases the pending entry is discarded and the pipeline is not
184    /// called, so the session is ready for a fresh entry.
185    ///
186    /// # Examples
187    ///
188    /// Feeding lines straight from `read_line`, terminators included:
189    ///
190    /// ```
191    /// use repl_lang::{Feed, Session, Status};
192    ///
193    /// let stdin_lines = ["let x =\r\n", "  42\n"];
194    /// let mut session = Session::new();
195    /// let mut last = None;
196    /// for raw in stdin_lines {
197    ///     last = Some(session.feed(raw, |input| {
198    ///         if input.text().trim_end().ends_with('=') {
199    ///             Status::Incomplete
200    ///         } else {
201    ///             Status::Complete(input.text().to_owned())
202    ///         }
203    ///     })?);
204    /// }
205    /// let Some(Feed::Complete { value, .. }) = last else { return Err("incomplete".into()) };
206    /// assert_eq!(value, "let x =\n  42\n");
207    /// # Ok::<(), Box<dyn std::error::Error>>(())
208    /// ```
209    ///
210    /// The pipeline can capture the interpreter state it needs, mutably:
211    ///
212    /// ```
213    /// use repl_lang::{Feed, Session, Status};
214    ///
215    /// let mut total = 0_i64;
216    /// let mut session = Session::new();
217    /// for line in ["5", "10", "-3"] {
218    ///     session.feed(line, |input| match input.text().trim().parse::<i64>() {
219    ///         Ok(n) => { total += n; Status::Complete(Ok(total)) }
220    ///         Err(e) => Status::Complete(Err(e)),
221    ///     })?;
222    /// }
223    /// assert_eq!(total, 12);
224    /// # Ok::<(), repl_lang::SessionError>(())
225    /// ```
226    pub fn feed<T, F>(&mut self, line: &str, pipeline: F) -> Result<Feed<T>, SessionError>
227    where
228        F: FnOnce(Input<'_>) -> Status<T>,
229    {
230        let line = strip_terminator(line);
231        if self.pending.is_empty() && line.trim().is_empty() {
232            return Ok(Feed::Empty);
233        }
234        self.admit(line)?;
235        self.pending.push_str(line);
236        self.pending.push('\n');
237
238        let number = self.next_number();
239        match pipeline(Input::new(&self.pending, self.next_base, number)) {
240            Status::Incomplete => Ok(Feed::Incomplete),
241            Status::Complete(value) => {
242                let entry = self.commit(number)?;
243                Ok(Feed::Complete { entry, value })
244            }
245        }
246    }
247
248    /// Discards the pending entry, returning `true` if there was one.
249    ///
250    /// This is the session half of an interrupt (`Ctrl-C`): drop the
251    /// half-typed entry and go back to the primary prompt. Committed entries
252    /// are unaffected and the next entry number is unchanged.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use repl_lang::{Session, Status};
258    ///
259    /// let mut session = Session::new();
260    /// session.feed("if x {", |_| Status::<()>::Incomplete)?;
261    /// assert!(session.is_pending());
262    /// assert!(session.cancel());
263    /// assert!(!session.is_pending());
264    /// assert!(!session.cancel());
265    /// # Ok::<(), repl_lang::SessionError>(())
266    /// ```
267    pub fn cancel(&mut self) -> bool {
268        let had = !self.pending.is_empty();
269        self.pending.clear();
270        had
271    }
272
273    /// Returns `true` while an entry is incomplete — when the next line
274    /// continues it rather than starting a new one.
275    ///
276    /// Hosts use it to choose between the primary prompt and the continuation
277    /// prompt.
278    ///
279    /// # Examples
280    ///
281    /// ```
282    /// use repl_lang::{Session, Status};
283    ///
284    /// let mut session = Session::new();
285    /// let prompt = |s: &Session| if s.is_pending() { "... " } else { ">>> " };
286    /// assert_eq!(prompt(&session), ">>> ");
287    /// session.feed("[1,", |_| Status::<()>::Incomplete)?;
288    /// assert_eq!(prompt(&session), "... ");
289    /// # Ok::<(), repl_lang::SessionError>(())
290    /// ```
291    #[inline]
292    #[must_use]
293    pub fn is_pending(&self) -> bool {
294        !self.pending.is_empty()
295    }
296
297    /// The text of the pending entry: every line fed since the last complete
298    /// entry, each ending in `'\n'`. Empty when nothing is pending.
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// use repl_lang::{Session, Status};
304    ///
305    /// let mut session = Session::new();
306    /// session.feed("a", |_| Status::<()>::Incomplete)?;
307    /// session.feed("b", |_| Status::<()>::Incomplete)?;
308    /// assert_eq!(session.pending(), "a\nb\n");
309    /// # Ok::<(), repl_lang::SessionError>(())
310    /// ```
311    #[inline]
312    #[must_use]
313    pub fn pending(&self) -> &str {
314        &self.pending
315    }
316
317    /// The number of committed entries.
318    ///
319    /// # Examples
320    ///
321    /// ```
322    /// use repl_lang::{Session, Status};
323    ///
324    /// let mut session = Session::new();
325    /// session.feed("one", |_| Status::Complete(()))?;
326    /// session.feed("", |_| Status::Complete(()))?; // blank: not an entry
327    /// assert_eq!(session.len(), 1);
328    /// # Ok::<(), repl_lang::SessionError>(())
329    /// ```
330    #[inline]
331    #[must_use]
332    pub fn len(&self) -> usize {
333        self.ids.len()
334    }
335
336    /// Returns `true` if no entry has been committed yet.
337    ///
338    /// # Examples
339    ///
340    /// ```
341    /// use repl_lang::Session;
342    ///
343    /// assert!(Session::new().is_empty());
344    /// ```
345    #[inline]
346    #[must_use]
347    pub fn is_empty(&self) -> bool {
348        self.ids.is_empty()
349    }
350
351    /// The committed entry with the given number (counting from `1`), or
352    /// `None` if there is no such entry.
353    ///
354    /// The returned [`SourceFile`] gives the entry's name (`<repl:N>`), its
355    /// text, and its global span — what a `:history` or `:show` command needs.
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// use repl_lang::{Session, Status};
361    ///
362    /// let mut session = Session::new();
363    /// session.feed("x = 1", |_| Status::Complete(()))?;
364    ///
365    /// let first = session.entry(1).ok_or("missing")?;
366    /// assert_eq!(first.name(), "<repl:1>");
367    /// assert_eq!(first.text(), "x = 1\n");
368    /// assert!(session.entry(0).is_none());
369    /// assert!(session.entry(2).is_none());
370    /// # Ok::<(), Box<dyn std::error::Error>>(())
371    /// ```
372    #[must_use]
373    pub fn entry(&self, number: u32) -> Option<&SourceFile> {
374        let index = usize::try_from(number).ok()?.checked_sub(1)?;
375        self.sources.source(*self.ids.get(index)?)
376    }
377
378    /// The source map holding every committed entry, one source per entry.
379    ///
380    /// Render diagnostics against it: any span the pipeline produced from an
381    /// [`Input`] resolves to the right entry, line, and column.
382    ///
383    /// # Examples
384    ///
385    /// ```
386    /// use diag_lang::{Diagnostic, Label, Renderer, Severity};
387    /// use repl_lang::{Session, Span, Status};
388    ///
389    /// let mut session = Session::new();
390    /// let diag = session.feed("let y = x + 1", |input| {
391    ///     // Pretend `x` is unbound: point at it with a global span.
392    ///     let at = input.base().to_u32() + 8;
393    ///     Status::Complete(Diagnostic::new(
394    ///         Severity::Error,
395    ///         "cannot find `x`",
396    ///         Label::new(Span::new(at, at + 1), "not found"),
397    ///     ))
398    /// })?;
399    /// let repl_lang::Feed::Complete { value: diag, .. } = diag else { return Err("pending".into()) };
400    ///
401    /// let text = Renderer::new().render(&diag, session.sources());
402    /// assert!(text.contains("<repl:1>:1:9"));
403    /// assert!(text.contains("^ not found"));
404    /// # Ok::<(), Box<dyn std::error::Error>>(())
405    /// ```
406    #[inline]
407    #[must_use]
408    pub fn sources(&self) -> &SourceMap {
409        &self.sources
410    }
411
412    /// The maximum byte length of one entry.
413    ///
414    /// # Examples
415    ///
416    /// ```
417    /// use repl_lang::Session;
418    ///
419    /// assert_eq!(Session::with_limit(4096).limit(), 4096);
420    /// ```
421    #[inline]
422    #[must_use]
423    pub fn limit(&self) -> usize {
424        self.limit
425    }
426
427    /// Checks that appending `line` (plus its `'\n'`) keeps the entry within
428    /// the size limit and the remaining position space. On failure the
429    /// pending entry is discarded.
430    fn admit(&mut self, line: &str) -> Result<(), SessionError> {
431        let len = self
432            .pending
433            .len()
434            .saturating_add(line.len())
435            .saturating_add(1);
436        let error = if len > self.limit {
437            SessionError::TooLong {
438                len,
439                limit: self.limit,
440            }
441        } else {
442            let available = u64::from(u32::MAX - self.next_base);
443            let needed = len as u64;
444            if needed <= available {
445                return Ok(());
446            }
447            SessionError::SpaceExhausted { needed, available }
448        };
449        self.pending.clear();
450        Err(error)
451    }
452
453    /// The number the pending entry will be committed as.
454    #[inline]
455    fn next_number(&self) -> u32 {
456        // Every entry occupies at least two bytes of the 32-bit position space,
457        // so the space runs out long before the count could reach `u32::MAX`;
458        // saturating keeps that reasoning out of the arithmetic.
459        u32::try_from(self.ids.len()).map_or(u32::MAX, |n| n.saturating_add(1))
460    }
461
462    /// Moves the pending entry into the source map as entry `number`.
463    fn commit(&mut self, number: u32) -> Result<Entry, SessionError> {
464        self.name.clear();
465        self.name.push_str("<repl:");
466        push_decimal(&mut self.name, number);
467        self.name.push('>');
468
469        let needed = self.pending.len() as u64;
470        let added = self.sources.add(self.name.as_str(), self.pending.as_str());
471        self.pending.clear();
472        let id = added.map_err(|_| SessionError::SpaceExhausted {
473            needed,
474            available: u64::from(u32::MAX - self.next_base),
475        })?;
476
477        // `admit` checked that the entry fits after `next_base`.
478        let len = needed as u32;
479        let span = Span::new(self.next_base, self.next_base + len);
480        self.next_base += len;
481        self.ids.push(id);
482        Ok(Entry::new(number, id, span))
483    }
484}
485
486/// Removes one trailing `"\n"` or `"\r\n"`.
487#[inline]
488fn strip_terminator(line: &str) -> &str {
489    match line.strip_suffix('\n') {
490        Some(rest) => rest.strip_suffix('\r').unwrap_or(rest),
491        None => line,
492    }
493}
494
495/// Appends the decimal digits of `n` without going through `core::fmt`.
496fn push_decimal(out: &mut String, mut n: u32) {
497    let mut digits = [0_u8; 10];
498    let mut start = digits.len();
499    loop {
500        start -= 1;
501        // `n % 10 < 10`, so the digit is ASCII.
502        digits[start] = b'0' + (n % 10) as u8;
503        n /= 10;
504        if n == 0 {
505            break;
506        }
507    }
508    for &digit in &digits[start..] {
509        out.push(char::from(digit));
510    }
511}
512
513#[cfg(test)]
514#[allow(clippy::unwrap_used, clippy::panic, unused_results)]
515mod tests {
516    use alloc::{borrow::ToOwned, string::ToString};
517
518    use super::*;
519
520    fn complete(input: Input<'_>) -> Status<String> {
521        Status::Complete(input.text().to_owned())
522    }
523
524    fn incomplete(_: Input<'_>) -> Status<String> {
525        Status::Incomplete
526    }
527
528    #[test]
529    fn test_feed_blank_line_with_nothing_pending_is_empty() {
530        let mut session = Session::new();
531        for blank in ["", "   ", "\n", "\r\n", "\t \n"] {
532            let feed = session.feed(blank, |_| -> Status<()> { panic!("pipeline called") });
533            assert_eq!(feed, Ok(Feed::Empty));
534        }
535        assert!(session.is_empty());
536    }
537
538    #[test]
539    fn test_feed_complete_commits_entry() {
540        let mut session = Session::new();
541        let feed = session.feed("x = 1", complete).unwrap();
542        let Feed::Complete { entry, value } = feed else {
543            panic!("expected complete, got {feed:?}");
544        };
545        assert_eq!(value, "x = 1\n");
546        assert_eq!(entry.number(), 1);
547        assert_eq!(entry.span(), Span::new(0, 6));
548        assert_eq!(session.len(), 1);
549        assert!(!session.is_pending());
550        let source = session.sources().source(entry.id()).unwrap();
551        assert_eq!(source.name(), "<repl:1>");
552        assert_eq!(source.text(), "x = 1\n");
553        assert_eq!(source.span(), entry.span());
554    }
555
556    #[test]
557    fn test_feed_incomplete_accumulates_lines() {
558        let mut session = Session::new();
559        assert_eq!(session.feed("a", incomplete), Ok(Feed::Incomplete));
560        assert_eq!(session.feed("", incomplete), Ok(Feed::Incomplete));
561        assert_eq!(session.feed("b", incomplete), Ok(Feed::Incomplete));
562        assert_eq!(session.pending(), "a\n\nb\n");
563        assert!(session.is_empty());
564    }
565
566    #[test]
567    fn test_feed_blank_line_inside_entry_reaches_pipeline() {
568        let mut session = Session::new();
569        session.feed("def f():", incomplete).unwrap();
570        let mut seen = String::new();
571        session
572            .feed("", |input| {
573                seen.push_str(input.text());
574                Status::Complete(())
575            })
576            .unwrap();
577        assert_eq!(seen, "def f():\n\n");
578    }
579
580    #[test]
581    fn test_feed_strips_one_terminator() {
582        let mut session = Session::new();
583        let texts: Vec<String> = ["a\n", "b\r\n", "c", "d\n\n", "e\r"]
584            .iter()
585            .map(|line| match session.feed(line, complete).unwrap() {
586                Feed::Complete { value, .. } => value,
587                other => panic!("{other:?}"),
588            })
589            .collect();
590        assert_eq!(texts, ["a\n", "b\n", "c\n", "d\n\n", "e\r\n"]);
591    }
592
593    #[test]
594    fn test_feed_bases_are_contiguous() {
595        let mut session = Session::new();
596        let mut bases = Vec::new();
597        for line in ["one", "two", "three"] {
598            session
599                .feed(line, |input| {
600                    bases.push(input.base().to_u32());
601                    Status::Complete(())
602                })
603                .unwrap();
604        }
605        assert_eq!(bases, [0, 4, 8]);
606        assert_eq!(session.entry(3).unwrap().span(), Span::new(8, 14));
607    }
608
609    #[test]
610    fn test_feed_number_stable_while_incomplete() {
611        let mut session = Session::new();
612        let mut numbers = Vec::new();
613        for (line, done) in [("a", false), ("b", true), ("c", true)] {
614            session
615                .feed(line, |input| {
616                    numbers.push(input.number());
617                    if done {
618                        Status::Complete(())
619                    } else {
620                        Status::Incomplete
621                    }
622                })
623                .unwrap();
624        }
625        assert_eq!(numbers, [1, 1, 2]);
626    }
627
628    #[test]
629    fn test_feed_too_long_discards_pending_and_skips_pipeline() {
630        let mut session = Session::with_limit(5);
631        session.feed("ab", incomplete).unwrap(); // 3 bytes
632        let err = session
633            .feed("cd", |_| -> Status<()> { panic!("pipeline called") })
634            .unwrap_err();
635        assert_eq!(err, SessionError::TooLong { len: 6, limit: 5 });
636        assert!(!session.is_pending());
637        // The session is usable again.
638        assert!(matches!(
639            session.feed("ok", complete),
640            Ok(Feed::Complete { .. })
641        ));
642    }
643
644    #[test]
645    fn test_feed_exactly_at_limit_is_accepted() {
646        let mut session = Session::with_limit(4);
647        assert!(matches!(
648            session.feed("abc", complete),
649            Ok(Feed::Complete { .. })
650        ));
651    }
652
653    #[test]
654    fn test_feed_zero_limit_refuses_everything_but_blanks() {
655        let mut session = Session::with_limit(0);
656        assert_eq!(session.feed("", complete), Ok(Feed::Empty));
657        assert!(matches!(
658            session.feed("x", complete),
659            Err(SessionError::TooLong { .. })
660        ));
661    }
662
663    #[test]
664    fn test_feed_space_exhausted_when_map_full() {
665        let mut session = Session::with_limit(usize::MAX);
666        session.next_base = u32::MAX - 3; // pretend 4 GiB of entries came before
667        let err = session.feed("abcd", complete).unwrap_err();
668        assert_eq!(
669            err,
670            SessionError::SpaceExhausted {
671                needed: 5,
672                available: 3
673            }
674        );
675        assert!(!session.is_pending());
676    }
677
678    #[test]
679    fn test_cancel_keeps_entry_number() {
680        let mut session = Session::new();
681        session.feed("x", incomplete).unwrap();
682        assert!(session.cancel());
683        let mut number = 0;
684        session
685            .feed("y", |input| {
686                number = input.number();
687                Status::Complete(())
688            })
689            .unwrap();
690        assert_eq!(number, 1);
691    }
692
693    #[test]
694    fn test_entry_out_of_range_is_none() {
695        let mut session = Session::new();
696        session.feed("x", complete).unwrap();
697        assert!(session.entry(0).is_none());
698        assert!(session.entry(2).is_none());
699        assert!(session.entry(u32::MAX).is_none());
700        assert_eq!(session.entry(1).unwrap().text(), "x\n");
701    }
702
703    #[test]
704    fn test_push_decimal_formats_like_display() {
705        for n in [0, 7, 10, 99, 100, 12_345, u32::MAX] {
706            let mut out = String::new();
707            push_decimal(&mut out, n);
708            assert_eq!(out, n.to_string());
709        }
710    }
711
712    #[test]
713    fn test_session_is_send_and_sync() {
714        fn assert_send_sync<T: Send + Sync>() {}
715        assert_send_sync::<Session>();
716        assert_send_sync::<crate::Editor>();
717    }
718}