Skip to main content

tess/
source.rs

1use std::borrow::Cow;
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom};
4use std::ops::Range;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex, atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}};
7use std::time::SystemTime;
8
9use crate::prettify::{self, PrettifyMode};
10
11pub trait Source: Send + Sync {
12    fn len(&self) -> usize;
13    fn is_empty(&self) -> bool { self.len() == 0 }
14    fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]>;
15    fn is_complete(&self) -> bool;
16    /// Read any new bytes that have become available since the last call.
17    /// Default no-op for static sources. Streaming sources override.
18    fn pump(&self) {}
19    /// Monotonic counter that bumps whenever the source's *content* has been
20    /// replaced wholesale (as opposed to merely appended to). Append-style
21    /// sources keep this at 0; `LiveFileSource` increments it on each detected
22    /// rewrite so the event loop knows to rebuild the line index instead of
23    /// just folding in new bytes.
24    fn revision(&self) -> u64 { 0 }
25
26    /// Current prettify mode if this source has a `TransformingSource` wrapper.
27    /// `None` means the source is not capable of prettification (e.g. a plain
28    /// `FileSource`). `Some(Off)` means wrapped but currently raw.
29    fn prettify_mode(&self) -> Option<PrettifyMode> { None }
30
31    /// Status-line label for the active prettify state, e.g. `"json"` or
32    /// `"json:err"`. `None` when there's no wrapper or the mode is `Off`
33    /// without an error.
34    fn prettify_label(&self) -> Option<String> { None }
35
36    /// Switch to a specific prettify mode. No-op for sources without a wrapper.
37    /// Bumps `revision()` so callers know to rebuild the line index.
38    fn set_prettify_mode(&self, _mode: PrettifyMode) {}
39
40    /// Flip between the current mode and the last active (non-Off) mode.
41    /// No-op if the source is not wrapped, or if it has never had an active
42    /// mode (i.e. only ever been raw).
43    fn toggle_prettify(&self) {}
44
45    /// Re-run byte-based content detection and apply the result. Used by the
46    /// interactive `-Pa` ("auto") sub-command. No-op without a wrapper.
47    fn redetect_prettify(&self) {}
48
49    /// Returns true once when the source detected that its backing file
50    /// was rotated or truncated. Append-style sources always return false.
51    /// `FileSource` overrides this and consumes the flag on each call so
52    /// the app loop only reacts once per event.
53    fn take_rotated(&self) -> bool { false }
54
55    /// Source's on-disk path, when one exists. Used by the app loop to
56    /// re-open the source after `take_rotated()` returns true. `None`
57    /// for stdin / mock sources.
58    fn path(&self) -> Option<&Path> { None }
59}
60
61/// Find the byte offset such that `bytes[offset..]` is exactly the last `n`
62/// logical lines of `src` (lines delimited by `\n`; a trailing `\n` at EOF is
63/// not its own line). Returns 0 when the source has fewer than `n` lines, or
64/// `src.len()` when `n == 0`.
65///
66/// Reverse-scans in 64 KiB chunks so a 10 GB file stays cheap — only the
67/// trailing pages need to be touched.
68pub fn find_tail_offset(src: &dyn Source, n: usize) -> usize {
69    let total = src.len();
70    if n == 0 || total == 0 {
71        return total;
72    }
73    // Don't count a trailing newline as a "line break to step over": it
74    // terminates the last line, it's not a separator before another line.
75    let mut end = total;
76    if end > 0 && src.bytes((end - 1)..end)[0] == b'\n' {
77        end -= 1;
78    }
79
80    let chunk_size: usize = 64 * 1024;
81    let mut count = 0usize;
82    let mut pos = end;
83    while pos > 0 {
84        let chunk_start = pos.saturating_sub(chunk_size);
85        let bytes = src.bytes(chunk_start..pos);
86        for i in (0..bytes.len()).rev() {
87            if bytes[i] == b'\n' {
88                count += 1;
89                if count == n {
90                    return chunk_start + i + 1;
91                }
92            }
93        }
94        pos = chunk_start;
95    }
96    0
97}
98
99pub struct FileSource {
100    mmap: Option<memmap2::Mmap>,
101    fallback_buf: Option<Vec<u8>>,
102    initial_size: usize,
103    appended_len: AtomicUsize,
104    streaming: Mutex<StreamingState>,
105    /// On-disk path, retained so the app can re-open after rotation.
106    path: PathBuf,
107    /// `(known_size, known_inode)` from the last successful pump. Used to
108    /// detect rotation (inode change) or truncation (size shrunk). Inode
109    /// is 0 on platforms without `MetadataExt` (Windows path is gated out
110    /// of follow-mode anyway).
111    known: Mutex<(u64, u64)>,
112    /// Set by `pump()` when it detects rotation/truncation. Read and
113    /// cleared by `take_rotated()`; the app loop responds by re-opening
114    /// the source from `path` and resetting the line index.
115    rotated: AtomicBool,
116}
117
118struct StreamingState {
119    file: File,
120    appended: Vec<u8>,
121}
122
123impl std::fmt::Debug for FileSource {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("FileSource").finish()
126    }
127}
128
129impl FileSource {
130    pub fn open(path: &Path) -> std::io::Result<Self> {
131        let file = File::open(path)?;
132        let metadata = file.metadata()?;
133        if !metadata.is_file() {
134            return Err(std::io::Error::new(
135                std::io::ErrorKind::InvalidInput,
136                "not a regular file",
137            ));
138        }
139        let initial_size = metadata.len() as usize;
140        let inode = inode_of(&metadata);
141        let (mmap, fallback_buf) = if initial_size == 0 {
142            (None, Some(Vec::new()))
143        } else {
144            // SAFETY: file remains open as long as Mmap exists; we never modify the file.
145            match unsafe { memmap2::Mmap::map(&file) } {
146                Ok(m) => (Some(m), None),
147                Err(_) => {
148                    let mut buf = Vec::new();
149                    let mut f = File::open(path)?;
150                    f.read_to_end(&mut buf)?;
151                    (None, Some(buf))
152                }
153            }
154        };
155        // Separate handle for streaming reads. Seeked past the initial content
156        // so subsequent reads only return bytes appended after open.
157        let mut stream_file = File::open(path)?;
158        stream_file.seek(SeekFrom::Start(initial_size as u64))?;
159        Ok(Self {
160            mmap,
161            fallback_buf,
162            initial_size,
163            appended_len: AtomicUsize::new(0),
164            streaming: Mutex::new(StreamingState {
165                file: stream_file,
166                appended: Vec::new(),
167            }),
168            path: path.to_path_buf(),
169            known: Mutex::new((initial_size as u64, inode)),
170            rotated: AtomicBool::new(false),
171        })
172    }
173
174    /// Retained on-disk path; the app loop uses this to re-open after
175    /// detecting rotation.
176    pub fn path(&self) -> &Path {
177        &self.path
178    }
179
180    /// Consume the rotation flag set by `pump()`. Returns `true` exactly
181    /// once per detected rotation/truncation event.
182    pub fn take_rotated(&self) -> bool {
183        self.rotated.swap(false, Ordering::AcqRel)
184    }
185
186    fn static_bytes(&self) -> &[u8] {
187        if let Some(m) = &self.mmap {
188            &m[..]
189        } else if let Some(b) = &self.fallback_buf {
190            &b[..]
191        } else {
192            &[]
193        }
194    }
195}
196
197impl Source for FileSource {
198    fn len(&self) -> usize {
199        self.initial_size + self.appended_len.load(Ordering::Acquire)
200    }
201
202    fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
203        let static_bytes = self.static_bytes();
204        if range.end <= self.initial_size {
205            return Cow::Borrowed(&static_bytes[range]);
206        }
207        let stream = self.streaming.lock().unwrap();
208        let total = self.initial_size + stream.appended.len();
209        let start = range.start.min(total);
210        let end = range.end.min(total);
211        if start >= self.initial_size {
212            let off = start - self.initial_size;
213            let off_end = end - self.initial_size;
214            Cow::Owned(stream.appended[off..off_end].to_vec())
215        } else {
216            let mut v = Vec::with_capacity(end - start);
217            v.extend_from_slice(&static_bytes[start..self.initial_size]);
218            v.extend_from_slice(&stream.appended[..end - self.initial_size]);
219            Cow::Owned(v)
220        }
221    }
222
223    fn is_complete(&self) -> bool { true }
224
225    fn take_rotated(&self) -> bool {
226        Self::take_rotated(self)
227    }
228
229    fn path(&self) -> Option<&Path> {
230        Some(&self.path)
231    }
232
233    fn pump(&self) {
234        // Detect rotation/truncation by stat'ing the path before reading.
235        // A shrinking file means it was truncated in place; a changed inode
236        // means it was rotated (renamed and re-created). Both want the
237        // caller to drop the current source and re-open from offset 0.
238        if let Ok(meta) = std::fs::metadata(&self.path) {
239            let new_size = meta.len();
240            let new_inode = inode_of(&meta);
241            let mut known = self.known.lock().unwrap();
242            let known_total = self.initial_size as u64 + self.appended_len.load(Ordering::Acquire) as u64;
243            let truncated = new_size < known_total;
244            let rotated = known.1 != 0 && new_inode != 0 && new_inode != known.1;
245            if truncated || rotated {
246                self.rotated.store(true, Ordering::Release);
247                // Stop reading from a stale handle; the caller will rebuild
248                // the source via FileSource::open(path).
249                known.0 = new_size;
250                known.1 = new_inode;
251                return;
252            }
253            known.0 = new_size;
254            known.1 = new_inode;
255        }
256
257        let mut stream = self.streaming.lock().unwrap();
258        let mut tmp = [0u8; 8192];
259        loop {
260            match stream.file.read(&mut tmp) {
261                Ok(0) => break,
262                Ok(n) => stream.appended.extend_from_slice(&tmp[..n]),
263                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
264                Err(_) => break,
265            }
266        }
267        let new_len = stream.appended.len();
268        self.appended_len.store(new_len, Ordering::Release);
269    }
270}
271
272#[cfg(unix)]
273fn inode_of(meta: &std::fs::Metadata) -> u64 {
274    use std::os::unix::fs::MetadataExt;
275    meta.ino()
276}
277
278#[cfg(not(unix))]
279fn inode_of(_meta: &std::fs::Metadata) -> u64 {
280    0
281}
282
283/// A test/utility source whose contents can be appended at runtime.
284pub struct MockSource {
285    buf: Arc<Mutex<Vec<u8>>>,
286    complete: Arc<AtomicBool>,
287}
288
289impl Default for MockSource {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295impl MockSource {
296    pub fn new() -> Self {
297        Self {
298            buf: Arc::new(Mutex::new(Vec::new())),
299            complete: Arc::new(AtomicBool::new(false)),
300        }
301    }
302
303    pub fn append(&self, more: &[u8]) {
304        self.buf.lock().unwrap().extend_from_slice(more);
305    }
306
307    pub fn finish(&self) {
308        self.complete.store(true, Ordering::SeqCst);
309    }
310}
311
312impl Source for MockSource {
313    fn len(&self) -> usize { self.buf.lock().unwrap().len() }
314    fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
315        Cow::Owned(self.buf.lock().unwrap()[range].to_vec())
316    }
317    fn is_complete(&self) -> bool { self.complete.load(Ordering::SeqCst) }
318}
319
320/// A file source that watches for *whole-file* rewrites. Unlike `FileSource`
321/// (which only picks up appended bytes via a streaming handle), `LiveFileSource`
322/// re-reads the entire file when its `(mtime, size, ino)` signature changes,
323/// swaps the buffer in place, and bumps a revision counter so callers know to
324/// rebuild any per-line state. Intended for source-file-sized inputs being
325/// rewritten by an editor or AI agent.
326pub struct LiveFileSource {
327    path: PathBuf,
328    state: Mutex<LiveState>,
329    revision: AtomicU64,
330}
331
332struct LiveState {
333    bytes: Vec<u8>,
334    signature: FileSignature,
335}
336
337#[derive(Clone, Copy, PartialEq, Eq, Debug)]
338struct FileSignature {
339    mtime: Option<SystemTime>,
340    size: u64,
341    ino: u64,
342}
343
344impl FileSignature {
345    fn read(path: &Path) -> std::io::Result<Self> {
346        let md = std::fs::metadata(path)?;
347        Ok(Self {
348            mtime: md.modified().ok(),
349            size: md.len(),
350            #[cfg(unix)]
351            ino: {
352                use std::os::unix::fs::MetadataExt;
353                md.ino()
354            },
355            #[cfg(not(unix))]
356            ino: 0,
357        })
358    }
359}
360
361impl std::fmt::Debug for LiveFileSource {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        f.debug_struct("LiveFileSource").field("path", &self.path).finish()
364    }
365}
366
367impl LiveFileSource {
368    pub fn open(path: &Path) -> std::io::Result<Self> {
369        let md = std::fs::metadata(path)?;
370        if !md.is_file() {
371            return Err(std::io::Error::new(
372                std::io::ErrorKind::InvalidInput,
373                "not a regular file",
374            ));
375        }
376        let bytes = std::fs::read(path)?;
377        // Re-stat after read so the signature reflects the bytes we actually
378        // captured (concurrent writers won't tear our buffer, but the mtime/size
379        // we record should match what we read).
380        let signature = FileSignature::read(path)?;
381        Ok(Self {
382            path: path.to_path_buf(),
383            state: Mutex::new(LiveState { bytes, signature }),
384            revision: AtomicU64::new(0),
385        })
386    }
387}
388
389impl Source for LiveFileSource {
390    fn len(&self) -> usize { self.state.lock().unwrap().bytes.len() }
391
392    fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
393        let s = self.state.lock().unwrap();
394        let end = range.end.min(s.bytes.len());
395        let start = range.start.min(end);
396        Cow::Owned(s.bytes[start..end].to_vec())
397    }
398
399    /// Live sources are never "complete" — the file may be rewritten at any
400    /// time. The status line picks this up as the `+` suffix on totals.
401    fn is_complete(&self) -> bool { false }
402
403    fn pump(&self) {
404        // Cheap stat to detect a change. If the file vanished, leave the
405        // current buffer in place and try again next tick.
406        let new_sig = match FileSignature::read(&self.path) {
407            Ok(sig) => sig,
408            Err(_) => return,
409        };
410        let mut s = self.state.lock().unwrap();
411        if new_sig == s.signature {
412            return;
413        }
414        let new_bytes = match std::fs::read(&self.path) {
415            Ok(b) => b,
416            Err(_) => return,
417        };
418        // Re-read signature after the read so we reflect what was actually loaded.
419        let post_sig = FileSignature::read(&self.path).unwrap_or(new_sig);
420        s.bytes = new_bytes;
421        s.signature = post_sig;
422        drop(s);
423        self.revision.fetch_add(1, Ordering::AcqRel);
424    }
425
426    fn revision(&self) -> u64 { self.revision.load(Ordering::Acquire) }
427}
428
429/// A source that wraps another source and applies a `PrettifyMode` transform
430/// to its bytes. Toggling the mode bumps `revision()` so the event loop
431/// rebuilds the line index. Falls back to passing through the inner bytes if
432/// the transform fails to parse, surfacing the error via `last_error()`.
433pub struct TransformingSource {
434    inner: Box<dyn Source>,
435    state: Mutex<TransformState>,
436    revision: AtomicU64,
437}
438
439struct TransformState {
440    mode: PrettifyMode,
441    /// Most recent non-Off mode. Used by `toggle_prettify` to flip back to
442    /// what the user was looking at most recently. Stays at the original
443    /// detected/explicit mode at startup, even if the user toggles to Off.
444    last_active: PrettifyMode,
445    /// Whatever the consumer should see — either the raw inner bytes or the
446    /// successfully transformed bytes. Always populated.
447    cached: Vec<u8>,
448    /// Set when the last attempted transform failed to parse. Cleared on a
449    /// successful transform or on switching to `Off`.
450    last_error: Option<String>,
451}
452
453impl std::fmt::Debug for TransformingSource {
454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455        f.debug_struct("TransformingSource").finish()
456    }
457}
458
459impl TransformingSource {
460    /// Build a wrapping source. The inner is read fully via its current bytes
461    /// view; the transform runs once. If the transform fails, the cache holds
462    /// the raw inner bytes and `last_error()` returns the parse error.
463    pub fn wrap(inner: Box<dyn Source>, mode: PrettifyMode) -> Self {
464        let raw = inner.bytes(0..inner.len()).to_vec();
465        let (cached, last_error) = run_transform(mode, &raw);
466        let last_active = if mode.is_active() { mode } else { PrettifyMode::Off };
467        Self {
468            inner,
469            state: Mutex::new(TransformState { mode, last_active, cached, last_error }),
470            revision: AtomicU64::new(0),
471        }
472    }
473
474    pub fn mode(&self) -> PrettifyMode {
475        self.state.lock().unwrap().mode
476    }
477
478    /// Currently surfaced parse error, if any. Cleared by a successful
479    /// `set_mode` or by switching to `Off`.
480    pub fn last_error(&self) -> Option<String> {
481        self.state.lock().unwrap().last_error.clone()
482    }
483
484    /// Internal apply-mode helper. Re-reads inner bytes, runs the transform,
485    /// updates the cache, bumps revision. Tracks `last_active` so toggle
486    /// can flip back to a non-Off mode.
487    fn apply_mode(&self, mode: PrettifyMode) {
488        let raw = self.inner.bytes(0..self.inner.len()).to_vec();
489        let (cached, last_error) = run_transform(mode, &raw);
490        let mut s = self.state.lock().unwrap();
491        s.mode = mode;
492        if mode.is_active() && last_error.is_none() {
493            s.last_active = mode;
494        }
495        s.cached = cached;
496        s.last_error = last_error;
497        drop(s);
498        self.revision.fetch_add(1, Ordering::AcqRel);
499    }
500}
501
502fn run_transform(mode: PrettifyMode, raw: &[u8]) -> (Vec<u8>, Option<String>) {
503    match prettify::prettify(mode, raw) {
504        Ok(out) => (out, None),
505        Err(e) => (raw.to_vec(), Some(e)),
506    }
507}
508
509impl Source for TransformingSource {
510    fn len(&self) -> usize { self.state.lock().unwrap().cached.len() }
511
512    fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
513        let s = self.state.lock().unwrap();
514        let end = range.end.min(s.cached.len());
515        let start = range.start.min(end);
516        Cow::Owned(s.cached[start..end].to_vec())
517    }
518
519    fn is_complete(&self) -> bool { self.inner.is_complete() }
520
521    fn pump(&self) { self.inner.pump(); }
522
523    fn revision(&self) -> u64 { self.revision.load(Ordering::Acquire) }
524
525    fn prettify_mode(&self) -> Option<PrettifyMode> {
526        Some(self.state.lock().unwrap().mode)
527    }
528
529    fn prettify_label(&self) -> Option<String> {
530        let s = self.state.lock().unwrap();
531        if s.last_error.is_some() {
532            // Whatever mode we tried last; show "<mode>:err".
533            let lbl = s.mode.label();
534            let lbl = if lbl.is_empty() { s.last_active.label() } else { lbl };
535            Some(format!("{lbl}:err"))
536        } else if s.mode.is_active() {
537            Some(s.mode.label().to_string())
538        } else {
539            None
540        }
541    }
542
543    fn set_prettify_mode(&self, mode: PrettifyMode) {
544        self.apply_mode(mode);
545    }
546
547    fn toggle_prettify(&self) {
548        let target = {
549            let s = self.state.lock().unwrap();
550            if s.mode.is_active() {
551                PrettifyMode::Off
552            } else if s.last_active.is_active() {
553                s.last_active
554            } else {
555                // Never had an active mode (started raw and never flipped on).
556                // Try a one-shot detection from the inner bytes; if nothing
557                // matches, leave state alone.
558                drop(s);
559                self.redetect_prettify();
560                return;
561            }
562        };
563        self.apply_mode(target);
564    }
565
566    fn redetect_prettify(&self) {
567        let raw = self.inner.bytes(0..self.inner.len()).to_vec();
568        let detected = crate::prettify::detect_from_bytes(&raw);
569        if let Some(mode) = detected {
570            self.apply_mode(mode);
571        }
572        // Undetected: leave state alone (caller will see same label).
573    }
574}
575
576pub struct StdinSource {
577    inner: StdinInner,
578}
579
580enum StdinInner {
581    Static(Vec<u8>),
582    Streaming {
583        buf: Arc<Mutex<Vec<u8>>>,
584        len_cache: Arc<AtomicUsize>,
585        complete: Arc<AtomicBool>,
586    },
587}
588
589impl StdinSource {
590    /// Read all of stdin into a buffer synchronously. After this returns,
591    /// stdin (fd 0) is at EOF; the caller is responsible for redirecting fd 0
592    /// to /dev/tty before entering raw mode if interactive input is needed.
593    pub fn read_all() -> std::io::Result<Self> {
594        let mut bytes = Vec::new();
595        std::io::stdin().lock().read_to_end(&mut bytes)?;
596        Ok(Self { inner: StdinInner::Static(bytes) })
597    }
598
599    /// Duplicate fd 0 onto a private fd, then spawn a thread that reads from
600    /// it into a shared buffer. Caller can safely `dup2(/dev/tty, 0)` afterwards
601    /// without disturbing the thread — it reads from the duplicated fd, not
602    /// from `STDIN_FILENO`.
603    #[cfg(unix)]
604    pub fn spawn_streaming() -> std::io::Result<Self> {
605        use std::os::unix::io::FromRawFd;
606        let cloned_fd = unsafe { libc::dup(libc::STDIN_FILENO) };
607        if cloned_fd < 0 {
608            return Err(std::io::Error::last_os_error());
609        }
610        // SAFETY: cloned_fd is now owned by the File; closed on Drop.
611        let mut file = unsafe { File::from_raw_fd(cloned_fd) };
612
613        let buf = Arc::new(Mutex::new(Vec::<u8>::new()));
614        let len_cache = Arc::new(AtomicUsize::new(0));
615        let complete = Arc::new(AtomicBool::new(false));
616        let buf_w = Arc::clone(&buf);
617        let len_w = Arc::clone(&len_cache);
618        let complete_w = Arc::clone(&complete);
619        std::thread::spawn(move || {
620            let mut tmp = [0u8; 8192];
621            loop {
622                match file.read(&mut tmp) {
623                    Ok(0) => break,
624                    Ok(n) => {
625                        let mut b = buf_w.lock().unwrap();
626                        b.extend_from_slice(&tmp[..n]);
627                        len_w.store(b.len(), Ordering::Release);
628                    }
629                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
630                    Err(_) => break,
631                }
632            }
633            complete_w.store(true, Ordering::SeqCst);
634        });
635        Ok(Self { inner: StdinInner::Streaming { buf, len_cache, complete } })
636    }
637}
638
639impl Source for StdinSource {
640    fn len(&self) -> usize {
641        match &self.inner {
642            StdinInner::Static(v) => v.len(),
643            StdinInner::Streaming { len_cache, .. } => len_cache.load(Ordering::Acquire),
644        }
645    }
646    fn bytes(&self, range: Range<usize>) -> Cow<'_, [u8]> {
647        match &self.inner {
648            StdinInner::Static(v) => Cow::Borrowed(&v[range]),
649            StdinInner::Streaming { buf, .. } => Cow::Owned(buf.lock().unwrap()[range].to_vec()),
650        }
651    }
652    fn is_complete(&self) -> bool {
653        match &self.inner {
654            StdinInner::Static(_) => true,
655            StdinInner::Streaming { complete, .. } => complete.load(Ordering::Acquire),
656        }
657    }
658}
659
660/// A `Source` backed by an in-memory `Vec<u8>`. Used by the preprocessor
661/// to feed transformed bytes into the viewer without writing a temp file.
662pub struct MemorySource {
663    bytes: Vec<u8>,
664}
665
666impl MemorySource {
667    pub fn new(bytes: Vec<u8>) -> Self {
668        Self { bytes }
669    }
670}
671
672impl Source for MemorySource {
673    fn len(&self) -> usize {
674        self.bytes.len()
675    }
676
677    fn bytes(&self, range: std::ops::Range<usize>) -> std::borrow::Cow<'_, [u8]> {
678        std::borrow::Cow::Borrowed(&self.bytes[range])
679    }
680
681    fn is_complete(&self) -> bool {
682        true
683    }
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689    use std::io::Write;
690
691    #[test]
692    fn file_source_reads_temp_file() {
693        let mut tmp = tempfile::NamedTempFile::new().unwrap();
694        tmp.write_all(b"hello world").unwrap();
695        let src = FileSource::open(tmp.path()).unwrap();
696        assert_eq!(src.len(), 11);
697        assert_eq!(&*src.bytes(0..5), b"hello");
698        assert_eq!(&*src.bytes(6..11), b"world");
699        assert!(src.is_complete());
700    }
701
702    #[test]
703    fn file_source_empty_file() {
704        let tmp = tempfile::NamedTempFile::new().unwrap();
705        let src = FileSource::open(tmp.path()).unwrap();
706        assert_eq!(src.len(), 0);
707    }
708
709    #[test]
710    fn file_source_directory_errors() {
711        let dir = tempfile::tempdir().unwrap();
712        let err = FileSource::open(dir.path()).unwrap_err();
713        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
714    }
715
716    #[test]
717    fn file_source_pump_picks_up_appended_bytes() {
718        let mut tmp = tempfile::NamedTempFile::new().unwrap();
719        tmp.write_all(b"first").unwrap();
720        tmp.flush().unwrap();
721        let src = FileSource::open(tmp.path()).unwrap();
722        assert_eq!(src.len(), 5);
723        // Append more bytes to the underlying file.
724        tmp.write_all(b" second").unwrap();
725        tmp.flush().unwrap();
726        // Before pump, len() reflects only what we knew at open.
727        assert_eq!(src.len(), 5);
728        src.pump();
729        assert_eq!(src.len(), 12);
730        // Borrowed range entirely in the original mmap.
731        assert_eq!(&*src.bytes(0..5), b"first");
732        // Range entirely in the appended region.
733        assert_eq!(&*src.bytes(5..12), b" second");
734        // Range straddling the boundary (3..10 = 7 bytes of "first second").
735        assert_eq!(&*src.bytes(3..10), b"st seco");
736    }
737
738    #[test]
739    fn find_tail_offset_zero_lines_returns_total() {
740        let m = MockSource::new();
741        m.append(b"a\nb\nc\n");
742        assert_eq!(find_tail_offset(&m, 0), 6);
743    }
744
745    #[test]
746    fn find_tail_offset_empty_source() {
747        let m = MockSource::new();
748        assert_eq!(find_tail_offset(&m, 5), 0);
749    }
750
751    #[test]
752    fn find_tail_offset_fewer_lines_than_n_returns_zero() {
753        let m = MockSource::new();
754        m.append(b"a\nb\nc\n");  // 3 lines
755        assert_eq!(find_tail_offset(&m, 10), 0);
756    }
757
758    #[test]
759    fn find_tail_offset_last_one_with_trailing_newline() {
760        let m = MockSource::new();
761        m.append(b"alpha\nbeta\ngamma\n");  // 3 lines
762        // gamma starts at byte 11.
763        assert_eq!(find_tail_offset(&m, 1), 11);
764    }
765
766    #[test]
767    fn find_tail_offset_last_two_with_trailing_newline() {
768        let m = MockSource::new();
769        m.append(b"alpha\nbeta\ngamma\n");
770        // beta starts at byte 6.
771        assert_eq!(find_tail_offset(&m, 2), 6);
772    }
773
774    #[test]
775    fn find_tail_offset_last_one_no_trailing_newline() {
776        let m = MockSource::new();
777        m.append(b"alpha\nbeta\ngamma");  // last line not terminated
778        // gamma starts at byte 11.
779        assert_eq!(find_tail_offset(&m, 1), 11);
780    }
781
782    #[test]
783    fn find_tail_offset_exactly_n_lines_returns_zero() {
784        let m = MockSource::new();
785        m.append(b"a\nb\nc\n");  // 3 lines exactly
786        assert_eq!(find_tail_offset(&m, 3), 0);
787    }
788
789    #[test]
790    fn live_source_reads_initial_content() {
791        let mut tmp = tempfile::NamedTempFile::new().unwrap();
792        tmp.write_all(b"alpha\nbeta\n").unwrap();
793        tmp.flush().unwrap();
794        let src = LiveFileSource::open(tmp.path()).unwrap();
795        assert_eq!(src.len(), 11);
796        assert_eq!(&*src.bytes(0..11), b"alpha\nbeta\n");
797        assert_eq!(src.revision(), 0);
798        assert!(!src.is_complete()); // live sources are always "growing"
799    }
800
801    #[test]
802    fn live_source_pump_picks_up_rewritten_content() {
803        let tmp = tempfile::NamedTempFile::new().unwrap();
804        std::fs::write(tmp.path(), b"first\n").unwrap();
805        let src = LiveFileSource::open(tmp.path()).unwrap();
806        assert_eq!(src.len(), 6);
807        assert_eq!(src.revision(), 0);
808
809        // Rewrite the file with different (longer) content. mtime should
810        // bump; signatures differ; pump() picks it up.
811        std::thread::sleep(std::time::Duration::from_millis(20));
812        std::fs::write(tmp.path(), b"second longer line\n").unwrap();
813        src.pump();
814        assert_eq!(src.len(), 19);
815        assert_eq!(&*src.bytes(0..19), b"second longer line\n");
816        assert_eq!(src.revision(), 1);
817    }
818
819    #[test]
820    fn live_source_pump_no_change_does_not_bump_revision() {
821        let tmp = tempfile::NamedTempFile::new().unwrap();
822        std::fs::write(tmp.path(), b"stable\n").unwrap();
823        let src = LiveFileSource::open(tmp.path()).unwrap();
824        let r0 = src.revision();
825        src.pump();
826        src.pump();
827        src.pump();
828        assert_eq!(src.revision(), r0);
829    }
830
831    #[test]
832    fn live_source_handles_file_shrink() {
833        let tmp = tempfile::NamedTempFile::new().unwrap();
834        std::fs::write(tmp.path(), b"longer initial content\n").unwrap();
835        let src = LiveFileSource::open(tmp.path()).unwrap();
836        assert!(src.len() > 5);
837        std::thread::sleep(std::time::Duration::from_millis(20));
838        std::fs::write(tmp.path(), b"x\n").unwrap();
839        src.pump();
840        assert_eq!(src.len(), 2);
841        assert_eq!(&*src.bytes(0..2), b"x\n");
842        assert_eq!(src.revision(), 1);
843    }
844
845    #[test]
846    fn live_source_handles_atomic_rename() {
847        // Most editors write atomically: write tmp file, rename over original.
848        // The inode changes; mtime/size likely change. pump() must follow.
849        let dir = tempfile::tempdir().unwrap();
850        let target = dir.path().join("file.txt");
851        std::fs::write(&target, b"original\n").unwrap();
852        let src = LiveFileSource::open(&target).unwrap();
853        assert_eq!(&*src.bytes(0..9), b"original\n");
854
855        std::thread::sleep(std::time::Duration::from_millis(20));
856        let staging = dir.path().join("file.txt.tmp");
857        std::fs::write(&staging, b"renamed in\n").unwrap();
858        std::fs::rename(&staging, &target).unwrap();
859        src.pump();
860        assert_eq!(src.len(), 11);
861        assert_eq!(&*src.bytes(0..11), b"renamed in\n");
862        assert_eq!(src.revision(), 1);
863    }
864
865    #[test]
866    fn live_source_rebuild_flow_against_line_index() {
867        use crate::line_index::LineIndex;
868
869        let tmp = tempfile::NamedTempFile::new().unwrap();
870        std::fs::write(tmp.path(), b"a\nb\nc\n").unwrap();
871        let src = LiveFileSource::open(tmp.path()).unwrap();
872
873        let mut idx = LineIndex::new();
874        idx.notice_new_bytes(&src);
875        assert_eq!(idx.line_count(), 3);
876        let r0 = src.revision();
877
878        // Rewrite to a 5-line file.
879        std::thread::sleep(std::time::Duration::from_millis(20));
880        std::fs::write(tmp.path(), b"one\ntwo\nthree\nfour\nfive\n").unwrap();
881        src.pump();
882        assert_ne!(src.revision(), r0, "revision must bump on rewrite");
883
884        // Caller's job to rebuild the index — mirror what app::rebuild_after_replace does.
885        idx = LineIndex::new();
886        idx.notice_new_bytes(&src);
887        assert_eq!(idx.line_count(), 5);
888        assert_eq!(&*src.bytes(idx.line_range(2, &src)), b"three");
889    }
890
891    #[test]
892    fn transforming_source_passes_through_when_off() {
893        let inner = MockSource::new();
894        inner.append(b"hello\nworld\n");
895        let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Off);
896        assert_eq!(&*t.bytes(0..t.len()), b"hello\nworld\n");
897        assert!(t.last_error().is_none());
898        assert_eq!(t.revision(), 0);
899    }
900
901    #[test]
902    fn transforming_source_emits_pretty_bytes_when_on() {
903        let inner = MockSource::new();
904        inner.append(b"{\"a\":1,\"b\":2}");
905        let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Json);
906        let out = t.bytes(0..t.len()).to_vec();
907        let s = String::from_utf8(out).unwrap();
908        assert!(s.contains("\"a\": 1"));
909        assert!(s.contains("\"b\": 2"));
910        assert!(t.last_error().is_none());
911    }
912
913    #[test]
914    fn transforming_source_revision_bumps_on_mode_change() {
915        let inner = MockSource::new();
916        inner.append(b"{\"x\":1}");
917        let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Off);
918        let r0 = t.revision();
919        t.set_prettify_mode(PrettifyMode::Json);
920        assert!(t.revision() > r0);
921        let r1 = t.revision();
922        t.set_prettify_mode(PrettifyMode::Off);
923        assert!(t.revision() > r1);
924    }
925
926    #[test]
927    fn transforming_source_falls_back_to_raw_on_parse_error() {
928        let inner = MockSource::new();
929        inner.append(b"not actually json");
930        let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Json);
931        // Cache holds raw bytes since the transform failed.
932        assert_eq!(&*t.bytes(0..t.len()), b"not actually json");
933        let err = t.last_error().expect("expected parse error to be surfaced");
934        assert!(err.contains("json"), "expected json in error, got: {err}");
935        // Status label reflects the error.
936        let label = t.prettify_label().expect("error → label should be set");
937        assert!(label.ends_with(":err"), "expected :err label, got: {label}");
938    }
939
940    #[test]
941    fn transforming_source_set_mode_recovers_from_error() {
942        let inner = MockSource::new();
943        inner.append(b"plain content");
944        let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Json);
945        assert!(t.last_error().is_some());
946        t.set_prettify_mode(PrettifyMode::Off);
947        assert!(t.last_error().is_none());
948        assert_eq!(&*t.bytes(0..t.len()), b"plain content");
949    }
950
951    #[test]
952    fn transforming_source_toggle_flips_between_active_and_off() {
953        let inner = MockSource::new();
954        inner.append(b"{\"x\":1}");
955        let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Json);
956        assert_eq!(t.mode(), PrettifyMode::Json);
957        t.toggle_prettify();
958        assert_eq!(t.mode(), PrettifyMode::Off);
959        t.toggle_prettify();
960        // Flipped back to last active.
961        assert_eq!(t.mode(), PrettifyMode::Json);
962    }
963
964    #[test]
965    fn transforming_source_redetect_picks_up_format() {
966        let inner = MockSource::new();
967        inner.append(b"<?xml version=\"1.0\"?><root/>");
968        let t = TransformingSource::wrap(Box::new(inner), PrettifyMode::Off);
969        // Initially raw; after redetect, should be XML.
970        t.redetect_prettify();
971        assert_eq!(t.mode(), PrettifyMode::Xml);
972    }
973
974    #[test]
975    fn live_source_directory_errors() {
976        let dir = tempfile::tempdir().unwrap();
977        let err = LiveFileSource::open(dir.path()).unwrap_err();
978        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
979    }
980
981    #[test]
982    fn mock_source_grows_and_finishes() {
983        let m = MockSource::new();
984        assert_eq!(m.len(), 0);
985        assert!(!m.is_complete());
986        m.append(b"abc");
987        assert_eq!(m.len(), 3);
988        assert_eq!(&*m.bytes(0..3), b"abc");
989        m.append(b"def");
990        assert_eq!(&*m.bytes(0..6), b"abcdef");
991        m.finish();
992        assert!(m.is_complete());
993    }
994
995    #[test]
996    fn memory_source_len_and_bytes() {
997        let src = MemorySource::new(b"hello world".to_vec());
998        assert_eq!(src.len(), 11);
999        let slice = src.bytes(0..5);
1000        assert_eq!(&*slice, b"hello");
1001        assert!(src.is_complete());
1002    }
1003
1004    #[test]
1005    fn memory_source_empty() {
1006        let src = MemorySource::new(Vec::new());
1007        assert_eq!(src.len(), 0);
1008        assert!(src.is_empty());
1009    }
1010}