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}