qem/lib.rs
1//! Qem is a cross-platform text engine for Rust applications that need fast
2//! file-backed reads, incremental line indexing, and responsive editing for
3//! very large documents.
4//!
5//! At its core, Qem combines mmap-backed access, sparse on-disk line indexes,
6//! and mutable rope or piece-table edit buffers so large-file workflows remain
7//! responsive without requiring full materialization up front.
8//!
9//! `Qem` is the project name, not an expanded acronym.
10//!
11//! # Picking the Right Layer
12//!
13//! - Use [`Document`] when your application already owns tab state, session
14//! state, and background-job orchestration.
15//! - Use [`DocumentSession`] when you want a backend-first session wrapper with
16//! generation tracking, async open/save helpers, forwarded viewport/edit
17//! helpers, status snapshots, and progress polling while still owning cursor
18//! and GUI behavior in your app.
19//! - Use [`EditorTab`] when you additionally want convenience cursor state on
20//! top of the same session machinery.
21//! - Most GUI frontends render visible rows through [`Document::read_viewport`]
22//! or [`DocumentSession::read_viewport`].
23//! - Legacy compatibility wrappers that silently swallow edit errors or return
24//! raw progress tuples remain available for migration only, but they are
25//! deprecated and hidden from the main rustdoc surface in favor of the
26//! typed/session-first APIs.
27//!
28//! # Recommended Entry Path
29//!
30//! For most frontend integrations, start with [`DocumentSession`].
31//!
32//! - Use [`ViewportRequest`], [`TextSelection`], [`TextRange`], and
33//! [`SearchMatch`] as the main typed values passed between your app state and
34//! Qem.
35//! - Prefer bounded reads such as [`Document::read_viewport`],
36//! [`Document::read_text`], and [`Document::read_selection`] over
37//! full-document materialization through [`Document::text_lossy`],
38//! [`DocumentSession::text`], or [`EditorTab::text`] in normal UI loops.
39//! - Prefer the typed session-facing surface:
40//! [`DocumentSession::loading_state`], [`DocumentSession::loading_phase`],
41//! [`DocumentSession::save_state`], [`DocumentSession::background_issue`],
42//! [`DocumentSession::take_background_issue`], [`DocumentSession::close_pending`],
43//! and the typed `try_*` edit helpers.
44//! - Treat [`DocumentSession::document_mut`], [`DocumentSession::set_path`],
45//! unconditional [`Document::compact_piece_table`], and the full-text helpers
46//! as advanced escape hatches for callers that intentionally manage those
47//! trade-offs themselves.
48//! - Reach for raw [`Document`] when your application deliberately owns tab
49//! state, background-job orchestration, and save lifecycle itself.
50//!
51//! # Frontend Integration Recipe
52//!
53//! A typical GUI or TUI loop looks like this:
54//!
55//! 1. Open a file with [`Document::open`] or [`DocumentSession::open_file_async`].
56//! 2. Poll [`DocumentSession::poll_background_job`] and cache
57//! [`DocumentSession::status`] or the more focused
58//! [`DocumentSession::loading_state`], [`DocumentSession::loading_phase`],
59//! [`DocumentSession::save_state`], [`DocumentSession::background_issue`],
60//! [`DocumentSession::take_background_issue`], [`DocumentSession::close_pending`], and
61//! [`Document::indexing_state`] values from the app loop. Load progress
62//! covers the asynchronous open path itself; once the document is ready,
63//! continued line indexing is reported separately through
64//! [`Document::indexing_state`]. If a background job fails or is
65//! intentionally discarded as stale, [`DocumentSession::background_issue`]
66//! keeps the last typed problem available even after the current
67//! [`BackgroundActivity`] returns to idle. If [`DocumentSession::close_file`]
68//! was requested while the session was busy, [`DocumentSession::close_pending`]
69//! exposes that deferred-close state until the active worker finishes.
70//! Call [`DocumentSession::take_background_issue`] after surfacing that
71//! problem to clear the retained issue explicitly.
72//! 3. Size scrollbars with [`Document::display_line_count`] while indexing is
73//! still in progress.
74//! 4. Render only the visible rows with [`Document::read_viewport`].
75//! 5. Query [`Document::edit_capability_at`] when you want to disable editing
76//! for positions that would exceed huge-file safety limits.
77//! Avoid full-text materialization in hot paths: [`Document::text_lossy`],
78//! [`DocumentSession::text`], and [`EditorTab::text`] build a fresh
79//! `String` for the entire current document.
80//! 6. Wait for [`DocumentSession::poll_background_job`] to finish before
81//! applying session/tab edit helpers. While a background open/save is
82//! active, those helpers return [`DocumentError::EditUnsupported`];
83//! [`DocumentSession::document_mut`] is an escape hatch for callers that
84//! coordinate that synchronization themselves. If it is used while busy,
85//! the in-flight worker result is discarded on the next poll instead of
86//! being applied over newer raw document changes. The same stale-result
87//! rule applies to [`DocumentSession::set_path`] while busy. If a deferred
88//! close was pending at the time, that new session state change also
89//! cancels the deferred close.
90//! 7. If the user closes a session/tab while it is still busy, keep polling:
91//! [`DocumentSession::close_file`] defers the actual close until the active
92//! background open/save completes instead of silently dropping that result.
93//! Failed background saves cancel that deferred close so the dirty document
94//! stays available for retry or explicit discard.
95//! 8. Treat the active [`DocumentSession::loading_state`] or
96//! [`DocumentSession::save_state`] path as authoritative while busy. Later
97//! async open/save requests are rejected until that first worker result is
98//! polled and applied. The actual file write runs in the background, but
99//! `save_async` still snapshots the current document before the worker
100//! starts, so very large edited buffers may make the call itself noticeable.
101//! 9. Keep GUI selections as [`TextSelection`] values, read them through
102//! [`Document::read_selection`], convert them through
103//! [`Document::text_range_for_selection`], or edit them directly with
104//! [`Document::try_replace_selection`], [`Document::try_delete_selection`],
105//! [`Document::try_cut_selection`], [`Document::try_backspace_selection`],
106//! or [`Document::try_delete_forward_selection`]. Literal search is exposed
107//! through [`Document::find_next`], [`Document::find_prev`],
108//! [`Document::find_all`], the compiled-query variants such as
109//! [`Document::find_all_query`], the bounded range/position helpers, and
110//! the session/tab wrappers as typed [`SearchMatch`] values.
111//! 10. For long-lived edited piece-table documents, prefer
112//! [`Document::maintenance_status`] or
113//! [`Document::maintenance_status_with_policy`] (or the session/tab
114//! wrappers) when the caller wants one explicit maintenance snapshot.
115//! [`Document::maintenance_action`] and
116//! [`DocumentMaintenanceStatus::recommended_action`] provide a lighter
117//! high-level decision when the frontend only needs to know whether to do
118//! idle maintenance now or wait for an explicit boundary.
119//! Run [`Document::run_idle_compaction`] or
120//! [`Document::run_idle_compaction_with_policy`] during idle time for
121//! deferred maintenance. Keep
122//! [`Document::compact_piece_table`] for explicit maintenance actions.
123//! 11. Then save through [`Document::save_to`],
124//! [`DocumentSession::save_async`], or [`DocumentSession::save_as_async`].
125//!
126//! ```no_run
127//! # #[cfg(not(feature = "editor"))]
128//! # fn main() {}
129//! # #[cfg(feature = "editor")]
130//! use qem::{DocumentSession, ViewportRequest};
131//! use std::path::PathBuf;
132//!
133//! # #[cfg(feature = "editor")]
134//! fn pump_frame(session: &mut DocumentSession, path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
135//! if session.current_path().is_none() && !session.is_busy() {
136//! session.open_file_async(path)?;
137//! }
138//!
139//! if let Some(result) = session.poll_background_job() {
140//! result?;
141//! }
142//!
143//! let status = session.status();
144//!
145//! if let Some(progress) = status.indexing_state() {
146//! println!(
147//! "indexing: {}/{} bytes",
148//! progress.completed_bytes(),
149//! progress.total_bytes()
150//! );
151//! }
152//!
153//! let viewport = session.read_viewport(ViewportRequest::new(0, 40).with_columns(0, 160));
154//! println!("scroll rows: {}", status.display_line_count());
155//! println!("visible rows this frame: {}", viewport.len());
156//!
157//! Ok(())
158//! }
159//! #
160//! # #[cfg(feature = "editor")]
161//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
162//! # let mut session = DocumentSession::new();
163//! # let path = PathBuf::from("huge.log");
164//! # pump_frame(&mut session, path)?;
165//! # Ok(())
166//! # }
167//! ```
168//!
169//! # Cargo Features
170//!
171//! - `editor` (default): enables the backend-first session wrapper
172//! [`DocumentSession`], the convenience cursor wrapper [`EditorTab`], and
173//! the related progress/save helper types.
174//!
175//! # Current Contract
176//!
177//! - UTF-8 and ASCII text are the primary stable fast path: open, viewport
178//! reads, edits, undo/redo, and saves are supported without transcoding.
179//! - Explicit encoding open/save is available through
180//! [`Document::open_with_encoding`] and [`Document::save_to_with_encoding`]
181//! plus the session/tab wrappers. For convenience, BOM-backed UTF-16 files
182//! can also use [`Document::open_with_auto_encoding_detection`]. For a more
183//! extensible contract, the same flows are also exposed through
184//! [`DocumentOpenOptions`], [`OpenEncodingPolicy`], and
185//! [`DocumentSaveOptions`].
186//! - Auto-detect open currently recognizes BOM-backed UTF-16 files and
187//! otherwise keeps the normal UTF-8/ASCII fast path. Callers that already
188//! know a likely legacy fallback can opt into "detect first, otherwise
189//! reinterpret as X" through [`DocumentOpenOptions`] and the session/tab
190//! convenience wrappers.
191//! - Non-UTF8 opens currently materialize into a rope-backed document instead
192//! of using the mmap fast path. Very large legacy-encoded files may therefore
193//! still be rejected until the broader encoding contract lands in a later
194//! release.
195//! - Preserve-save for some decoded encodings can still return a typed
196//! [`DocumentError::Encoding`] with a structured
197//! [`DocumentEncodingErrorKind`] until a broader persistence contract lands.
198//! [`Document::decoding_had_errors`] means Qem has already seen malformed
199//! source bytes, but preserve-save is only rejected when the write would
200//! materialize lossy-decoded text. Raw mmap/piece-table preserve can still
201//! remain valid, while rope-backed legacy opens and UTF-8 after lossy
202//! materialization/edit correctly fail with
203//! [`DocumentEncodingErrorKind::LossyDecodedPreserve`]. Frontends can
204//! preflight both preserve and explicit conversion paths through
205//! [`Document::preserve_save_error`], [`Document::save_error_for_options`],
206//! and the matching session/tab wrappers before attempting the write.
207//! Callers can already convert to a supported target through
208//! [`DocumentSaveOptions`] or [`Document::save_to_with_encoding`].
209//! - Huge files are supported for mmap-backed reads, viewport rendering, line
210//! counting, and background indexing without full materialization. Editing
211//! may be rejected when it would require rope materialization beyond the
212//! built-in safety limits.
213//! - Typed positions, ranges, and viewport columns use document text units.
214//! For UTF-8 text, line-local columns count Unicode scalar values rather
215//! than grapheme clusters or display cells. Stored CRLF still counts as one
216//! text unit between lines.
217//! - Internal `.qem.lineidx` and `.qem.editlog` sidecars are validated against
218//! source file length, modification time, and a sampled content fingerprint.
219//! Their formats are internal cache/durability details rather than stable
220//! interchange formats, so Qem may rebuild, discard, or version-bump them
221//! across releases.
222//! - Session-facing async-open state is reported through byte progress plus an
223//! explicit [`LoadPhase`] so frontends can distinguish "open is still being
224//! prepared" from "the document is ready but background indexing continues".
225//! - Session-facing background failures and stale-result discards are retained
226//! as typed [`BackgroundIssue`] values so frontends can keep showing the most
227//! recent async-open/save problem after background activity has gone idle.
228//! Call [`DocumentSession::take_background_issue`] or
229//! [`EditorTab::take_background_issue`] when your app wants to acknowledge
230//! and clear that retained issue explicitly.
231//! - Deferred closes are part of the public session contract:
232//! [`DocumentSession::close_pending`] and the corresponding status snapshot
233//! expose when `close_file()` is waiting for an in-flight background job to
234//! finish before the document can actually disappear.
235
236pub mod document;
237#[cfg(feature = "editor")]
238pub mod editor;
239pub mod index;
240pub(crate) mod piece_tree;
241pub(crate) mod source_identity;
242pub mod storage;
243
244pub use document::{
245 ByteProgress, CompactionPolicy, CompactionRecommendation, CompactionUrgency, CutResult,
246 Document, DocumentBacking, DocumentEncoding, DocumentEncodingErrorKind, DocumentEncodingOrigin,
247 DocumentError, DocumentMaintenanceStatus, DocumentOpenOptions, DocumentSaveOptions,
248 DocumentStatus, EditCapability, EditResult, FragmentationStats, IdleCompactionOutcome,
249 LineCount, LineEnding, LineSlice, LiteralSearchIter, LiteralSearchQuery, MaintenanceAction,
250 OpenEncodingPolicy, SaveEncodingPolicy, SearchMatch, TextPosition, TextRange, TextSelection,
251 TextSlice, Viewport, ViewportRequest, ViewportRow,
252};
253#[cfg(feature = "editor")]
254pub use editor::{
255 BackgroundActivity, BackgroundIssue, BackgroundIssueKind, CursorPosition, DocumentSession,
256 DocumentSessionStatus, EditorTab, EditorTabStatus, FileProgress, LoadPhase, SaveError,
257};
258pub use storage::{FileStorage, StorageOpenError};