Skip to main content

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//! # Official Integration Demos
176//!
177//! The repository workspace contains two `egui` demos that exercise the
178//! frontend-facing path without pulling GUI dependencies into the library
179//! crate itself:
180//!
181//! - `qem-egui-demo`: minimal viewer/editor integration.
182//! - `qem-egui-demo --bin large_file`: large-file viewport integration with
183//!   gutter, explicit viewport window, caret, open/save, and visible
184//!   load/save/index status.
185//!
186//! # Current Support Contract
187//!
188//! ## UTF-8 and ASCII
189//!
190//! - UTF-8 / ASCII text is the primary stable fast path. Open, viewport reads,
191//!   edits, undo/redo, and save are supported without transcoding.
192//! - Huge-file reads use the mmap-oriented path when possible. Frontends
193//!   should treat this as the main scalable contract for text viewing.
194//! - Typed positions, ranges, selections, and viewport columns use document
195//!   text units. For UTF-8 text, line-local columns count Unicode scalar
196//!   values, not grapheme clusters and not display cells.
197//! - Stored CRLF still counts as one text unit between lines for typed
198//!   range/edit/navigation semantics.
199//!
200//! ## Invalid UTF-8 and Other Encodings
201//!
202//! - Explicit legacy-encoding open/save is supported through
203//!   [`Document::open_with_encoding`], [`Document::save_to_with_encoding`],
204//!   and the matching session/tab wrappers.
205//! - Auto-detect open currently recognizes BOM-backed UTF-16 files.
206//!   Otherwise Qem stays on the normal UTF-8 / ASCII path unless the caller
207//!   provides an explicit fallback through [`DocumentOpenOptions`].
208//! - Non-UTF8 opens currently materialize into a rope-backed document instead
209//!   of using the mmap fast path. Very large legacy-encoded files may still be
210//!   rejected until the wider encoding contract expands.
211//! - [`Document::decoding_had_errors`] means the source required lossy decode
212//!   replacement at open time. That does not automatically mean preserve-save
213//!   is forbidden.
214//! - Preserve-save is rejected only when the write would materialize
215//!   lossy-decoded text. Callers can preflight this through
216//!   [`Document::preserve_save_error`] / [`Document::save_error_for_options`]
217//!   and explicitly convert through [`DocumentSaveOptions`] or
218//!   [`Document::save_to_with_encoding`].
219//!
220//! ## Large Files and Edit Limits
221//!
222//! - Large files are supported for mmap-backed reads, viewport rendering,
223//!   line-count estimation, and background indexing without full
224//!   materialization.
225//! - Editing is allowed only when Qem can do it without violating built-in
226//!   safety limits. If an edit would require an unsafe promotion or full
227//!   materialization, Qem returns [`DocumentError::EditUnsupported`].
228//! - Frontends should use [`Document::edit_capability_at`],
229//!   [`Document::edit_capability_for_range`], or
230//!   [`Document::edit_capability_for_selection`] when they need to surface
231//!   that boundary before the user commits the action.
232//! - [`Document::display_line_count`] is the supported scroll-sizing value
233//!   while indexing is still in progress. Exact total line count may arrive
234//!   later through [`Document::indexing_state`] and the line-count status
235//!   helpers.
236//!
237//! ## Session and Background Job Guarantees
238//!
239//! - Typed session/status APIs such as [`DocumentSession::loading_state`],
240//!   [`DocumentSession::loading_phase`], [`DocumentSession::save_state`],
241//!   [`DocumentSession::background_issue`],
242//!   [`DocumentSession::take_background_issue`], and
243//!   [`DocumentSession::close_pending`] are the supported frontend-facing
244//!   async surface.
245//! - [`DocumentSession`] and [`EditorTab`] typed edit helpers are idle-only.
246//!   While a background open/save is active they return
247//!   [`DocumentError::EditUnsupported`] instead of mutating state under an
248//!   in-flight worker result.
249//! - [`DocumentSession::close_file`] is truthful. If a background open/save is
250//!   still running, close is deferred until that job completes instead of
251//!   silently dropping the worker result.
252//! - Repeated async open/save requests use first-job-wins semantics. While a
253//!   load/save is active, later requests are rejected until
254//!   [`DocumentSession::poll_background_job`] consumes the active result.
255//! - Raw [`DocumentSession::document_mut`] and [`DocumentSession::set_path`]
256//!   are escape hatches. Using them while busy invalidates the in-flight
257//!   worker result and turns the next poll into a discard/error path instead
258//!   of applying stale state.
259//!
260//! ## Search and Typed Reads
261//!
262//! - Literal search is part of the current public contract through
263//!   [`Document::find_next`], [`Document::find_prev`], [`Document::find_all`],
264//!   [`LiteralSearchQuery`], and the bounded query/range helpers.
265//! - This is a typed, case-sensitive literal search surface. It is not a
266//!   regex subsystem.
267//! - Bounded search returns only matches fully contained within the requested
268//!   typed range or boundary positions.
269//!
270//! ## Sidecars and Recovery
271//!
272//! - `.qem.lineidx` and `.qem.editlog` are internal sidecars used for
273//!   cache/recovery behavior.
274//! - Qem validates them against file length, modification time, and sampled
275//!   content fingerprint. When they do not match, Qem may rebuild them,
276//!   discard them, or reopen cleanly instead of trusting stale state.
277//! - Sidecar recovery behavior is public. Sidecar on-disk format is not.
278//!
279//! ## Public Behavior vs Internal Format
280//!
281//! - Stable public behavior in this release line includes the typed API
282//!   surface, open/save lifecycle, async progress semantics, huge-file read
283//!   contract, edit rejection semantics, and typed line/column rules.
284//! - Internal implementation details include sidecar binary layout, cache
285//!   structure, exact storage layout, and backing/layout decisions that are
286//!   not explicitly promised by the typed API.
287
288pub mod document;
289#[cfg(feature = "editor")]
290pub mod editor;
291pub mod index;
292pub(crate) mod piece_tree;
293pub(crate) mod source_identity;
294pub mod storage;
295
296pub use document::{
297    ByteProgress, CompactionPolicy, CompactionRecommendation, CompactionUrgency, CutResult,
298    Document, DocumentBacking, DocumentEncoding, DocumentEncodingErrorKind, DocumentEncodingOrigin,
299    DocumentError, DocumentMaintenanceStatus, DocumentOpenOptions, DocumentSaveOptions,
300    DocumentStatus, EditCapability, EditResult, FragmentationStats, IdleCompactionOutcome,
301    LineCount, LineEnding, LineSlice, LiteralSearchIter, LiteralSearchQuery, MaintenanceAction,
302    OpenEncodingPolicy, RegexCompileError, RegexSearchIter, RegexSearchQuery, SaveEncodingPolicy,
303    SearchMatch, TextPosition, TextRange, TextSelection, TextSlice, Viewport, ViewportRequest,
304    ViewportRow,
305};
306#[cfg(feature = "editor")]
307pub use editor::{
308    BackgroundActivity, BackgroundIssue, BackgroundIssueKind, CursorPosition, DocumentSession,
309    DocumentSessionStatus, EditorTab, EditorTabStatus, FileProgress, LoadPhase, SaveError,
310};
311pub use storage::{FileStorage, StorageOpenError};