# Changelog

All notable changes to this project will be documented in this file. The format
is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This
project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.41.0] - 2026-08-04

### Fixed

- `rope_line_str` and `rope_line_bytes` no longer leave a non-`\n` line
  separator in the row's content. ropey's `unicode_lines` splits on lone `\r`,
  U+000B, U+000C, U+0085 and U+2028 / U+2029, but the first stripped a literal
  `'\n'` and the second subtracted a hard-coded one byte — so a `\r`-terminated
  row reported one char too many, and U+2028 kept two of its three bytes. Both,
  and the internal `rope_line_char_count`, now share one rule: the row's content
  ends at the separator's first byte, found by flooring
  `line_to_byte(row + 1) - 1` to a char boundary. `\r\n` is unchanged and keeps
  its `\r`, matching `rope_row_range_str`.

- A corrupt undofile can no longer hang the editor on load.
  `UndoTree::from_serializable` checked that every link index was in range but
  not that the links described a tree, so a projection whose `parent` pointers
  formed a cycle loaded successfully — and `materialize` follows `parent` in an
  unbounded loop, so the first use of such a tree spun while growing its path
  vector. `Buffer::install_recovered_undo_tree` (the swap-recovery path)
  materializes on load, so a corrupt swap file reached it. The loader now
  rejects a projection unless the `children` lists partition the non-root nodes,
  each child agrees with the node that listed it, and every node is reachable
  from the root. It also rejects a repeated `seq`, which used to silently drop a
  node from the `g-` / `g+` index while leaving it in the arena. All of these
  return `None`, so the file degrades to a fresh tree as any other structural
  inconsistency already did.

### Performance

- Holding `g-` / `:earlier 9999` over a deep history is ~2.6x faster: a
  1024-state walk drops from 4.378 ms to 1.659 ms (`benches/undo.rs`,
  `cold_jump_back/1024`). Two costs, both paid once per history step:
  - `UndoTree::set_node_state` compared the stashed rope against the node's
    cached one with `Rope`'s `PartialEq`, which walks the whole document. It now
    tries `Rope::is_instance` (an `Arc::ptr_eq`, O(1)) first, which is the case
    that actually arises — the caller hands back the very rope the previous step
    gave it. An unrelated-but-equal rope still falls through to the full
    compare, so the result is unchanged. This is the larger share: 3.968 ms ->
    1.659 ms on its own.
  - `g-` / `g+` targets came from a full arena scan for the neighbouring `seq`.
    A `BTreeMap` seq index answers both directions in O(log N): 4.378 ms ->
    3.968 ms.

- A single history step (`g-` / `g+` / `:earlier` / `:later`) no longer scales
  with history depth at all. `UndoTree::retarget_current` rewrote `last_child`
  down the entire root→target chain on every landing; it now walks only from the
  landing target up to the fork with the path it was already on, which is one
  node for a step along a branch. Measured with the history teardown outside the
  timed region (`benches/undo.rs`, `single_deep_jump_no_drop`): 190.80 ns ->
  92.058 ns at depth 64, 744.73 ns -> 97.974 ns at 256, and 2.5387 us -> 101.29
  ns at 1024 — i.e. flat in depth instead of linear. Holding `g-` over a
  1024-state history (`cold_jump_back/1024`) drops from 1.5507 ms to 552.55 us
  as a result.

  What makes the shortcut sound — an early exit on a local "this ancestor
  already points the right way" test is NOT, and was tried — is that
  `UndoNode::on_path` now marks exactly the root→`current` chain as a maintained
  invariant, so every ancestor above the fork is correct by construction rather
  than by inspection. `path_flags_track_the_root_to_current_walk` pins that
  against a brute-force parent walk after every operation that moves `current`,
  allocates or frees, and the two random-op stress tests check it every step.

- New bench case `single_deep_jump_no_drop`. The existing `single_deep_jump`
  passes its `View` to the timed closure by value, so criterion runs the
  destructor of the whole N-node history inside the measurement; that O(N)
  teardown dominates the jump and made the case read as depth-scaling regardless
  of the jump's own cost. The new case uses `iter_batched_ref` so the batch is
  dropped after the timer stops. `single_deep_jump` itself is unchanged, so its
  recorded numbers stay comparable.

### Breaking

- **`View::set_auto_folds` takes `foldlevelstart: u32` instead of
  `default_closed: bool`.** The flag could only express "all closed" / "all
  open", so vim's intermediate levels were unreachable. The new parameter is
  vim's `'foldlevelstart'` verbatim: a fold whose 1-based nesting level exceeds
  it starts closed. Levels are computed inside `set_auto_folds` from the ranges'
  containment — the caller passes no per-range depth, because only this function
  knows which ranges survive normalisation (single-row, inverted and
  out-of-bounds entries dropped, `end_row` clamped, duplicate start rows
  collapsed, rows owned by a manual fold skipped), and a level counted over the
  raw ranges would be counting folds that do not exist. Migration:
  `default_closed: true` → `0`, `default_closed: false` → `99`. Everything else
  is unchanged — an existing auto fold still keeps its open/closed state across
  a reparse (`foldlevelstart` decides how a fold _starts_), manual (`zf`) folds
  are still never taken over and do not count as a nesting level, and an
  unchanged fold set still writes nothing and bumps no generation. Level
  semantics verified against neovim 0.12.4 at `foldlevelstart` 0/1/2/3/99,
  enumerating closed folds with `foldclosed` / `foldclosedend`.

### Fixed

- `geom::char_col_to_visual_col` and `geom::visual_col_to_char_col` are now
  wide-character aware. They counted every non-tab char as one cell; both now
  measure with `unicode-width`, exactly as `hjkl-buffer-tui`'s `paint_row` does.
  Consequences for callers: a tab's stop is computed from the visual column (so
  a preceding CJK char shifts it), a visual column falling in the trailing cell
  of a double-width glyph maps back to that glyph, and zero-width chars
  (combining marks, `U+FE0F`) contribute nothing and are never returned by
  `visual_col_to_char_col`. Verified against neovim 0.12.4. Control chars
  measure one cell and emoji presentation sequences measure per-`char`, both
  deliberate divergences from vim documented on the `geom` module.

## [0.40.0] - 2026-08-01

### Fixed

- `wrap::wrap_segments` no longer loops forever (growing an unbounded `Vec`
  until the process runs out of memory) when a single character is wider than
  the wrap width — e.g. a double-width CJK/emoji character in a 1-cell text
  area. The oversized character is emitted as its own segment so wrapping always
  makes forward progress.

### Removed

- Dropped the `ratatui` feature and `render` module. The ratatui Widget adapter
  now lives in the new `hjkl-buffer-tui` crate. Part of #162.

## [0.8.1] - 2026-05-18

### Fixed

- `lines()` and `line()` now return owned data (`Vec<String>` and
  `Option<String>`) instead of references backed by a held `MutexGuard`. The
  previous implementation used an unsafe lifetime extension that was unsound
  under multi-view use: a second `Buffer` sharing the same `Arc<Mutex<Content>>`
  could reallocate the `Vec` between guard-drop and caller use. Returning owned
  data eliminates the hazard with no API behaviour change for single-view users.
- `folds()` and `fold_at_row()` apply the same fix: `folds()` returns
  `Vec<Fold>` (cloned from the guard) and `fold_at_row()` returns `Option<Fold>`
  (copy-on-find, `Fold: Copy`). All unsafe blocks removed.
- `Arc<RefCell<Content>>` → `Arc<Mutex<Content>>` in module-level doc comment;
  concurrency rationale updated to match the actual implementation.

## [0.8.0] - 2026-05-16

## [0.7.0] - 2026-05-16

### Added

- `over_provisioned_range(line_count, top, height) -> Range<usize>` in the
  `viewport` module (re-exported from the crate root). Computes a 3×-viewport
  row range (one viewport above + current + one viewport below), clamped to the
  buffer's line count. Host-agnostic: takes only document line count and
  viewport extents — no terminal cells or pixels. Future GUI hosts call the same
  function with their own viewport dims. Previously this math lived inline in
  `apps/hjkl/src/app/syntax_glue.rs` (duplicated twice). Covered by 6 unit tests
  including boundary cases (top-of-buffer, bottom-of-buffer, zero height, empty
  buffer, and the invariant that the result always covers the original
  viewport).
- `is_big_viewport_jump(prev_top, cur_top, viewport_height) -> bool` — detects
  when a viewport scroll lands beyond the 3× over-provisioned band (jump >
  `viewport_height` in either direction). Hosts use this to decide whether to
  block briefly on a fresh parse so `gg` / `G` / `<C-d>` / `:N` don't flash
  un-highlighted rows. Host-agnostic: pure math, no terminal cells or pixels.
  Covered by 2 unit tests (within-band and out-of-band, including the boundary
  case: exactly `== height` is NOT a big jump; `height + 1` IS).
- `Gutter::sign_column_width: u16` field (default `0`). When non-zero, the
  gutter reserves the leftmost `sign_column_width` columns exclusively for signs
  (`paint_signs` writes into `area.x..area.x + sign_column_width`); line numbers
  are offset right by `sign_column_width` cells and text starts after the full
  gutter. Fixes 5-digit line number corruption where a `~` sign char would
  overwrite the leading digit (e.g. `~3109` instead of `13109`). Default `0`
  preserves the previous layout.

### Fixed

- Empty rows in a visual selection now paint a placeholder space cell carrying
  `selection_bg` for all three visual kinds (Char / Line / Block). Previously
  the char loop iterated zero chars on empty rows and the selection marker was
  lost, breaking the visual highlight across blank lines.
- Block-selection placeholder on empty rows now spans the full block column
  range (`lo..=hi` clipped to `row_end_x`) instead of a single col-0 cell.
  Previously the rectangle broke visually — empty rows in the middle of a column
  block showed nothing at the block's actual column range. Char and Line
  selections continue to paint a single col-0 marker.

## [0.6.2] - 2026-05-16

### Added

- `visual_col_to_char_col(line, visual_col, tab_width) -> usize` in the new
  `geom` module (re-exported from the crate root). Inverse of `hjkl-engine`'s
  internal `visual_col_for_char`: walks a line's chars accumulating tab-expanded
  visual width and returns the char index where a mouse click at `visual_col`
  lands. Tabs snap to the tab character itself (Vim behaviour). Past-EOL clicks
  clamp to `char_count`. Enables host-driven mouse translation without baking
  terminal-layout assumptions into the engine. See
  `geom::visual_col_to_char_col` rustdoc for the full contract and wide-char
  note.

## [0.6.1] - 2026-05-16

### Changed

- `resolve_span_style` now layers every overlapping span instead of picking one.
  Spans are sorted broadest-first, then `Style::patch`-merged so the narrower
  span's set fields override the broader span's, but unset fields (e.g. a narrow
  `@keyword` carrying only `fg`) inherit the broader span's values (e.g. a wide
  `@markup.raw.block` carrying only `bg`). This matches vim and Helix's layered
  hi-group model and lets hosts tint markdown code blocks (or any nested region)
  without bloating every injected language's captures with the same background.

  The pre-0.6.1 narrowest-wins-completely behaviour is preserved whenever the
  narrower span sets every field — `Style::patch` only carries broader fields
  through `None` slots.

### Tests

- `layered_spans_blend_broad_bg_with_narrow_fg` pins the new contract: broad
  bg-only span + narrow fg-only span ⇒ cells carry both.
- `narrow_span_with_explicit_bg_still_overrides_broad_bg` pins the override
  case: a narrow span that DOES set bg overrides the broader bg.

## [0.6.0] - 2026-05-10

### Added

- `BufferView` gains `colorcolumn_cols: &'a [u16]` (sorted, deduplicated 1-based
  column indices to highlight) and `colorcolumn_style: Style` (background style
  applied to those cells).
- Renderer paints those columns under syntax in a new pass between the
  cursorcolumn pass and the diagnostic overlay. `Wrap::None` only.

### Changed

- **Breaking**: `BufferView` is a `pub struct` without `#[non_exhaustive]`;
  adding required fields breaks any downstream that builds it via literal struct
  expression. Pass `colorcolumn_cols: &[]` and
  `colorcolumn_style: Style::default()` to keep prior behaviour.

## [0.5.0] - 2026-05-06

### Added

- `BufferView::non_text_style` field; rows past end-of-buffer now paint `~`
  (vim's NonText marker) at the first text column. The gutter on those rows
  stays blank. Style defaults to `Style::default()` when not set.
- `DiagOverlay` struct (`row`, `col_start`, `col_end`, `style`) for inline
  diagnostic highlighting. Applied in a post-paint pass so it layers on top of
  syntax and selection colours.
- `BufferView::diag_overlays: &'a [DiagOverlay]` field. Existing
  exhaustive-struct-init consumers must add `diag_overlays: &[]`. Pass `&[]` to
  disable (no behaviour change).

## [0.4.0] - 2026-05-06

### Added

- `GutterNumbers` enum (`Absolute` / `Relative` / `Hybrid` / `None`) controls
  what is rendered in the gutter. `Absolute` is the default, matching the
  previous always-on behaviour.
- `Gutter::numbers: GutterNumbers` field. Existing consumers using
  `..Default::default()` keep working; consumers using exhaustive struct-init
  syntax must add `numbers: GutterNumbers::Absolute` (minor break).
- `GutterNumbers` is re-exported from the crate root under the `ratatui` feature
  alongside `Gutter`.

## [0.3.5] - 2026-05-05

### Docs

- Inlined former `IMPLEMENTERS.md` invariants into rustdoc on the actual types
  and methods (`Position`, `Edit` + variants, `Fold`, `Viewport`, `Span`,
  `Buffer::set_cursor` / `clamp_position` / `ensure_cursor_visible`,
  `BufferView` render module, crate-level `lib.rs`). Now renders on docs.rs next
  to each symbol and shows up in IDE hover.
- Removed `IMPLEMENTERS.md` (content fully relocated; README points at docs.rs).
- Dropped stale Marks + Search sections — those APIs were removed from `Buffer`
  at v0.0.37 (now live in the engine layer).
- Fixed broken `MIGRATION.md` link in crate-level rustdoc (file was deleted
  upstream pre-0.1.0).
- Fixed three pre-existing broken intra-doc links in `render.rs`.

## [0.3.4] - 2026-05-04

### Docs

- Internal CHANGELOG hygiene: backfilled missing release entries and added
  reference link definitions for all version headings. No functional changes.

## [0.3.3] - 2026-05-03

### Docs

- Dropped frozen / sealed rhetoric from the README status section. Per the org's
  "no SPEC frozen claims" stance: features keep landing, bumps follow semver —
  no need to oversell stability.

## [0.3.2] - 2026-05-03

### Internal

- Dropped reference to `hjkl-engine/SPEC.md` from `src/motion.rs` doc comment.

## [0.3.1] - 2026-04-30

### Changed

- Migrated `hjkl-buffer` from the `kryptic-sh/hjkl` monorepo into its own
  repository
  ([kryptic-sh/hjkl-buffer](https://github.com/kryptic-sh/hjkl-buffer)) with
  full git history preserved.
- Relaxed inter-crate dependency requirements from `=0.3.0` to `0.3` (caret),
  matching the standard SemVer pattern for library dependencies.
- Bumped `ratatui` to 0.30 (was 0.29) and `criterion` to 0.8 (was 0.5).

### Added

- Standalone `LICENSE`, `.gitignore`, and `ci.yml` workflow at the repo root.

[Unreleased]: https://github.com/kryptic-sh/hjkl-buffer/compare/v0.7.0...HEAD
[0.8.1]: https://github.com/kryptic-sh/hjkl-buffer/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/kryptic-sh/hjkl-buffer/releases/tag/v0.8.0
[0.7.0]: https://github.com/kryptic-sh/hjkl-buffer/compare/v0.6.2...v0.7.0
[0.6.2]: https://github.com/kryptic-sh/hjkl-buffer/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/kryptic-sh/hjkl-buffer/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/kryptic-sh/hjkl-buffer/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/kryptic-sh/hjkl-buffer/releases/tag/v0.5.0
[0.4.0]: https://github.com/kryptic-sh/hjkl-buffer/releases/tag/v0.4.0
[0.3.5]: https://github.com/kryptic-sh/hjkl-buffer/releases/tag/v0.3.5
[0.3.4]: https://github.com/kryptic-sh/hjkl-buffer/releases/tag/v0.3.4
[0.3.3]: https://github.com/kryptic-sh/hjkl-buffer/releases/tag/v0.3.3
[0.3.2]: https://github.com/kryptic-sh/hjkl-buffer/releases/tag/v0.3.2
[0.3.1]: https://github.com/kryptic-sh/hjkl-buffer/releases/tag/v0.3.1
