hjkl-engine 0.41.2

Vim FSM, motion grammar, and ex commands. Pre-1.0 churn.
Documentation
//! Trait-surface cast helpers shared between [`crate::editor`] and the
//! discipline crates (`hjkl-vim`) that drive it.
//!
//! Promoted from `editor.rs` in 0.0.42 (Patch C-δ.7) so the vim free
//! functions can route their `ed.buffer().*` reaches through the
//! `Cursor` / `Query` / `BufferEdit` trait surface using the same cast
//! primitives the editor body uses. Mirrors the pattern lifted into
//! `motions.rs` in 0.0.40.
//!
//! All helpers take a generic `B: <trait> + ?Sized` so they compile
//! against the in-tree `hjkl_buffer::View` and the engine's mock
//! buffers (used by motion / search / vim trait-routing tests). The
//! `Pos { line: u32, col: u32 }` ⇄ `Position { row: usize, col: usize }`
//! cast lives at the boundary so call sites stay terse.

use crate::types::{Cursor, Query};

/// Read the cursor as a `(row, col)` `usize` tuple — the shape every
/// editor / vim free fn body expects. One inline cast at the trait
/// boundary.
#[inline]
pub fn buf_cursor_rc<B: Cursor + ?Sized>(b: &B) -> (usize, usize) {
    let p = Cursor::cursor(b);
    (p.line as usize, p.col as usize)
}

/// Read the cursor row.
#[inline]
pub fn buf_cursor_row<B: Cursor + ?Sized>(b: &B) -> usize {
    Cursor::cursor(b).line as usize
}

/// Read the cursor as an `hjkl_buffer::Position` — the shape the
/// concrete-buffer call sites consumed before the trait routing.
#[inline]
pub fn buf_cursor_pos<B: Cursor + ?Sized>(b: &B) -> hjkl_buffer::Position {
    let p = Cursor::cursor(b);
    hjkl_buffer::Position::new(p.line as usize, p.col as usize)
}

/// Set the cursor from `(row, col)` `usize` coordinates.
#[inline]
pub fn buf_set_cursor_rc<B: Cursor + ?Sized>(b: &mut B, row: usize, col: usize) {
    Cursor::set_cursor(
        b,
        crate::types::Pos {
            line: row as u32,
            col: col as u32,
        },
    );
}

/// Set the cursor from a concrete `hjkl_buffer::Position`. Routes the
/// `ed.buffer_mut().set_cursor(Position::new(...))` call sites in
/// `vim.rs` through the trait surface without a dedicated helper at
/// each site.
#[inline]
pub fn buf_set_cursor_pos<B: Cursor + ?Sized>(b: &mut B, pos: hjkl_buffer::Position) {
    buf_set_cursor_rc(b, pos.row, pos.col);
}

/// Number of rows.
#[inline]
pub fn buf_row_count<B: Query + ?Sized>(b: &B) -> usize {
    Query::line_count(b) as usize
}

/// Return line `row` as an owned `String`, or `None` for out-of-bounds.
/// `Query::line` returns owned data; callers that only need `&str` should
/// deref the result with `as_deref()`.
#[inline]
pub fn buf_line<B: Query + ?Sized>(b: &B, row: usize) -> Option<String> {
    let n = Query::line_count(b) as usize;
    if row >= n {
        return None;
    }
    Some(Query::line(b, row as u32))
}

/// Length (chars) of `row`. Returns 0 for out-of-bounds rows so call
/// sites that previously did
/// `buf.line(r).map(|l| l.chars().count()).unwrap_or(0)` collapse to
/// one call.
///
/// Counts through a rope snapshot ([`Query::rope`] is an O(1) Arc-clone
/// for the canonical [`hjkl_buffer::View`]) borrowed via
/// [`crate::viewport_math::rope_line_slice`], so no per-row `String` is
/// materialized. This runs on every `j`/`k` from the vim motion /
/// curswant / editor-ext paths.
#[inline]
pub fn buf_line_chars<B: Query + ?Sized>(b: &B, row: usize) -> usize {
    let rope = Query::rope(b);
    // `rope_line_slice` panics out-of-bounds (ropey contract); guard like the
    // old `buf_line`'s `row >= line_count` check. A trailing `\n` synthesizes
    // a phantom final empty row, same as `Query::line_count`.
    if row >= rope.len_lines() {
        return 0;
    }
    crate::viewport_math::rope_line_slice(&rope, row)
        .chars()
        .count()
}

/// Length (bytes) of `row`. Returns 0 for out-of-bounds rows. The
/// byte-shape mirror of [`buf_line_chars`] — used by call sites that
/// pre-0.0.42 inspected `buf.lines()[row].len()`.
///
/// Delegates to [`Query::line_bytes`] so backends with row-indexed
/// storage skip the per-row `String` clone the default walk would do.
#[inline]
pub fn buf_line_bytes<B: Query + ?Sized>(b: &B, row: usize) -> usize {
    Query::line_bytes(b, row)
}

/// Apply a [`hjkl_buffer::Edit`] and return the inverse for undo.
///
/// 0.0.42 (Patch C-δ.7): the `apply_edit` reach is intentionally kept
/// against the concrete `&mut hjkl_buffer::View` rather than lifted
/// onto a trait method. Rationale:
///
/// - `hjkl_buffer::Edit` is the rich buffer-side enum (~8 variants —
///   `InsertChar`, `InsertStr`, `DeleteRange`, `JoinLines`,
///   `SplitLines`, `Replace`, `InsertBlock`, `DeleteBlockChunks`)
///   with ~700 LOC of `do_*` machinery in `hjkl-buffer`. Lifting it
///   onto `BufferEdit` would require either an associated `Edit` type
///   (forces every backend to design its own rich-edit enum just to
///   compile) or duplicating the 8 variants on the trait surface
///   (busts the discipline cap).
/// - `crate::types::Edit` is a separate value type (`Range<Pos>` +
///   `String` replacement) used by the change-log emitter; it's
///   intentionally simpler and lossy for block / join / split ops.
///
/// Centralizing the reach in this free fn keeps `Editor::mutate_edit`
/// trait-shaped at the call site (no `self.buffer.<inherent>` hop in
/// the editor body) and gives 0.1.0 a single seam to flip when the
/// `B: View` generic lands.
///
/// The 0.1.0 design will introduce
/// `BufferEdit::apply_edit(&mut self, op: Self::Edit) -> Self::Edit`
/// with `type Edit;` so backends pick their own edit enum. This free
/// fn forwards there once that lands.
#[inline]
pub fn apply_buffer_edit(
    buf: &mut hjkl_buffer::View,
    edit: hjkl_buffer::Edit,
) -> hjkl_buffer::Edit {
    buf.apply_edit(edit)
}

#[cfg(test)]
mod tests {
    use super::*;
    use hjkl_buffer::View;

    /// `buf_line_chars` answers 0 for out-of-bounds rows — the old
    /// `buf_line(b, row).map_or(0, |l| l.chars().count())` contract —
    /// rather than panicking the rope.
    #[test]
    fn buf_line_chars_out_of_bounds_row_is_zero() {
        let b = View::from_str("foo\nbar");
        assert_eq!(buf_line_chars(&b, 0), 3);
        assert_eq!(buf_line_chars(&b, 1), 3);
        assert_eq!(buf_line_chars(&b, 2), 0);
        assert_eq!(buf_line_chars(&b, 999), 0);
    }
}