edamame 0.1.5

A fast TUI Markdown editor and viewer
Documentation
1
2
3
4
5
6
7
8
9
10
# Table widths and wrapping

> Part of the edamame contributor deep-dives. Index and project-wide conventions: [`AGENTS.md`](../../AGENTS.md). Sibling docs live in [`docs/dev/`](.).

- **Every table width is in terminal cells, measured by `table_layout::str_cells` / `char_cells` (`cells_of` for a char slice).** That is the per-char sum `line_render::paint_row` draws — not `chars().count()` (a CJK glyph is two cells) and not `UnicodeWidthStr::width`, which is sequence-aware and counts `❤️` and a ZWJ family as two cells each while the painter draws one and six. Column max (`Renderer::rendered_inlines_width`), column min (`renderer::table::cell_min_width`), row padding and truncation (`render_table_row`) and the drag anchors (`table_drag::natural_widths`) all go through it. Mixing units sizes a column for one width and draws it at another: a char-counted max pushed every CJK row's right border one cell per glyph (#45). A new width decision in this area uses `str_cells`, nothing else.
- **There is one wrap, `table_layout::wrap_ranges`.** The renderer's styled cells (`util::wrap_styled_chars`), the click mapper (`coord::table_click_to_raw_col`) and the wrapped raw-reveal overlay (via `wrap_cell_with_indices`) all call it, so their rows agree by construction. Don't add a parallel tokenizer — the two copies that existed before had already drifted on leading whitespace after a hard split. The raw-reveal chunk overlay (`cell_overlay::compute_cell_chunk_overlay`) deliberately hard-wraps instead — its chunks must depend only on glyph widths — but through `table_layout::hard_wrap_ranges`, which shares the cluster-boundary splitter (`split_at_clusters`) with the word wrap's hard split. A punctuation-preferring cut is taken only when the remainder still fits beside the next cluster, so no row exceeds its width except one holding a single cluster wider than the column.
- **Words split at soft spaces and beside wide glyphs.** `word_ranges` breaks at whitespace (NBSP excepted: code-span pads must travel with their token) and at every `Joint::Glyph` — a boundary of a cluster wider than one cell — because CJK carries no inter-word spaces. A wrap never cuts at `Joint::Inside`, so a grapheme cluster is never split, even by a hard split. `cell_min_width` and `table_drag::longest_word_cells` floor on the same `word_ranges`, so a CJK run floors at its widest glyph and the floor always matches what the wrap can actually achieve.
- **Rendered pipe positions come in two units, and each consumer needs a specific one.** `rendered_pipe_positions` returns char indices into the rendered line, which is what a `cursor_col_override` takes (`table_raw_col_to_rendered_col`, `WrappedCellOverlay::visual_col`). `rendered_pipe_cells` returns screen cells, which is what anything addressing the screen needs: `overlay_raw_cell` (`CellOverlay::rendered_start` / `rendered_end`), the click mapper `coord::table_click_to_raw_col` (a click column is a cell), the resize / cell hit map (`TableLayoutSnapshot::col_ranges`), and the wrap width of `table_raw_col_range_to_rendered_segments`. They diverge once a wide glyph sits left of the cell. `CellOverlay::cursor_in_cell` and the selection columns stay char indices into `raw_text`; the painter walks chars while advancing by cells, and blanks a glyph that would straddle the border rather than overwrite it.
- **A Preview cell band is in cells; convert it per line.** `CellBand::cols` is a cell range because every sub-line of a table row shares cell geometry, while its char columns shift sub-line by sub-line with the wide glyphs in other columns. Copy, the selection size, drag clamping and the Preview painter each call `CellBand::char_cols(line)` for the line at hand; never compare `cols` against a char column directly.
- **Highlight painters take char columns and place them by cells.** `line_render::patch_char_cols`, the one painter behind Rendered search / selection (`paint_cols_on_line`) and Preview selection, walks the row's chars from its start, adding `char_cells`, and styles both cells of a wide glyph. Offsetting `x` by the char index is what put CJK highlights in the wrong place, in paragraphs as well as tables.