# Vim commands inside a table
> Part of the edamame contributor deep-dives. Index and project-wide conventions: [`AGENTS.md`](../../AGENTS.md). Sibling docs live in [`docs/dev/`](.).
A rendered GFM table's `|` delimiters and alignment row are auto-managed chrome, not prose, so the vim layer treats the *cell* as the unit a motion moves within and the *row* as the unit a line command acts on. All of it lives in `editor::vim_ops::table`.
- **Raw mode is exempt, and gets that for free** — every query funnels through `table_edit_ops::current_table`, which returns `None` in `Mode::Raw`.
- **The scoped resolvers replace the bare ones at the input layer.** `resolve_scoped_motion` / `resolve_scoped_op_range` stand in for `motion::resolve_motion` / `resolve_motion_range` throughout `feed.rs`, rather than each call site clamping the result, so a new operator target can't forget the clamp; `motion.rs` stays pure and buffer-only. The check is `rg 'resolve_motion(_range)?\(' src/input/vim/feed.rs`, which should match nothing. `;` / `,` resolve through `resolve_find_repeat` and apply `scope_offset` by hand — the one exception.
- **Clamping shapes the keystroke; `range_breaks_a_table` is the safety net.** A range reaches a header or alignment row by plenty of routes with no cell to clamp against (`2dd`, `dj`, a VisualLine selection whose cursor left the table, a charwise selection carried down by `j`, `Vp`, Visual `r`), so the guard is asked of the **range**, immediately before the mutation, at the two funnels every vim range edit passes through — `run_operator` and `run_visual_operator` — plus the paste and replace sites. A new vim mutation path must call it too. `Yank` is never refused; it mutates nothing.
- **A charwise Visual highlight must cover only the cell's content — horizontally.** The span includes the char under the cursor, so the *horizontal* clamp is one grapheme tighter than in Normal: `CellLimit::LastChar` (vs. `Append`) stops `$`/`w`/`f` on the cell's last character, and `h`/`l` route through `visual_cell_step`; `feed.rs` derives both from the single predicate `is_charwise_visual`. Operator ranges keep `Append` (exclusive end — `D` must clear the whole cell). The guarantee is horizontal only: `j`/`k` and the deliberately unscoped document motions still leave the cell, which is what the range guard is for.
- **The two halves answer different failures.** Crossing a `|` promises an edit the guard refuses, so there the clamp keeps the highlight honest. Reaching the append slot is *not* refused: `table_break` measures confinement against `Cell::content_end` (the **untrimmed** span between the pipes) while `CellScope::end` is trimmed past the last non-blank, so the padding between them is inside the cell as far as the guard is concerned and the edit silently eats it — cosmetic rather than structural, but the clamp is the only thing preventing it.
- **Both doors into charwise Visual owe the same append-slot pull-back, on both ends of the span.** `enter_visual` (`v`) and `toggle_visual_mode` (`V`→`v`) each call `visual_endpoint_in_cell`, which resolves against the *endpoint's own* cell rather than the cursor's — `V`, `$`, `v` parks the cursor on the slot, and an `o` in between puts the anchor there instead, in a different cell. `v` needs only the cursor half; the toggle pulls cursor and anchor and re-syncs `EditorState::selection`, the span the operators actually read (`visual_charwise_range`), which still holds the pre-pull active end. A third entry into `VimSubMode::Visual` would owe all of it.
- **The guard refuses only what actually breaks.** Deleting a table *whole* is allowed, as are complete data rows, an edit confined to one cell's content, and an edit confined to the alignment row's own text (that row stays hand-editable, which is why it has no cell scope). Everything else — a partial span over a protected row, a range crossing a `|` — is refused with the reason on the flash line.
- **`dd`/`cc`'s structural routing is a convenience, not the protection.** `table_doubled_operator` re-routes an uncounted `dd` onto `table_edit`'s row delete and `cc` onto a cell clear, both through `execute_operator`/`fold_op_result` so register, single-delta undo and Insert transition come from the existing implementation. Counted forms deliberately fall through to plain linewise — safe *because* the funnel guard catches them. `TableOpOutcome` keeps "in a table, refused" distinct from "not a table, fall through"; collapsing them into an `Option` is what let `cc` blank the alignment row.
- **`p` picks a legal row boundary.** The ordinary linewise landing spot is *between the header and the alignment row* when the cursor is on the header, so `table_paste_plan` clamps the target index below the alignment row and refuses a register that isn't table rows (or a charwise one carrying a `|`). Raw mode keeps the plain landing spot.
- **Visual `p` asks about the payload, not the register.** `run_visual_paste` builds `text` first and hands `paste_over_range_refusal` that string plus the selection's linewise flag; passing `vim.register.{text,linewise}` lets both mismatches through (a `dd`'d row over a charwise in-cell selection carries `|` and a newline mid-cell; a charwise prose register over a VisualLine row replaces that row with a non-row). A new paste path owes the guard the bytes it is about to write.
- **Only the table's own lines are read out of the rope.** `table_edit_ops::table_at` slices the contiguous run of table-looking lines around a byte offset rather than calling `Buffer::contents()` — `cell_scope` is on the per-keystroke motion path.