Skip to content

Harden editor rendering, plugins, LSP, and undo#68

Merged
fcoury merged 38 commits into
masterfrom
codex/editor-e2e-improvements
May 28, 2026
Merged

Harden editor rendering, plugins, LSP, and undo#68
fcoury merged 38 commits into
masterfrom
codex/editor-e2e-improvements

Conversation

@fcoury
Copy link
Copy Markdown
Contributor

@fcoury fcoury commented May 9, 2026

Summary

Red has several editor paths where terminal geometry, Unicode display width, split-window state, and multi-buffer editing all interact. Those are the exact cases that make a modal editor feel unstable: UI chrome shifts when focus changes, wide characters break alignment, small terminals can underflow layout math, and edits in one buffer can leak into another buffer's undo state. This PR hardens those surfaces while also making the editor easier to extend through configurable LSP servers and persistent plugin panels.

The net change makes rendering and cursor math display-width aware, makes window-local rendering depend on each window's own buffer state, adds a NeoTree-style file panel through new plugin panel APIs, modularizes LSP server resolution, and replaces the old undo stack with buffer-local edit transactions plus redo support.

What Changed

  • Hardened TUI rendering for cramped terminals, wide Unicode text, diagnostics, completions, dialogs, info tooltips, overlays, picker/list rows, commandline cursor placement, statusline width calculations, and render-buffer boundary writes.
  • Added shared Unicode/display-width helpers and routed rendering, truncation, padding, commandline, overlay, list, completion, and cursor-placement code through those helpers where byte length or character count was unsafe.
  • Fixed split-window rendering and state handling, including active-window synchronization, cursor rendering against the active window's buffer line, sibling close behavior, nested split resizing, and inactive-window gutter widths that previously shifted when focus moved between buffers with different line counts.
  • Modularized LSP support so language servers are resolved from config, with rust-analyzer supplied as a default instead of a hard-coded singleton.
  • Added plugin panel APIs, runtime bindings, focus handling, and a NeoTree-style file tree plugin toggled with Ctrl-e.
  • Fixed movement edge cases around file-end clamping, word movement viewport preservation, partial-page movement, $ in normal and visual modes, and insert-mode escape at line end.
  • Reworked undo/redo around per-buffer edit transactions, added Ctrl-r redo, and tracks dirty revision checkpoints so undo can clear the modified indicator when a buffer returns to its saved revision.
  • Updated README/config/plugin documentation for redo, configurable LSP servers, and plugin panels.

How to Test

  1. Start Red with a narrow terminal and a file containing emoji or CJK text, then open completions, diagnostics, command/search input, plugin overlays, and picker/list UI. Confirm text is clipped or padded by display width without panics, broken alignment, or cursor drift.
  2. Open a split with buffers that have different line counts, leave the larger buffer inactive, and scroll to a two-digit line number such as line 10. Confirm the inactive gutter is already padded correctly and does not shift when focus moves to that window.
  3. Open a file, press Ctrl-e to toggle the NeoTree panel, move selection in the panel, activate a file or directory, press Esc, and confirm focus returns to the editor.
  4. Edit two buffers, save one of them, perform undo/redo with u and Ctrl-r, then confirm each buffer keeps independent history and the modified indicator clears when undo returns to the saved revision.
  5. Exercise split-window navigation and closing, including closing the first sibling in a split and resizing nested splits, and confirm the remaining window and active cursor state stay correct.

Targeted tests:

  • cargo test test_inactive_window_uses_its_own_gutter_width
  • cargo fmt --check
  • cargo test

fcoury added 30 commits May 8, 2026 01:12
Remove the duplicate test-only action reducer so integration tests execute the same editor dispatch path as production.

Tighten cursor, line-boundary, and Unicode handling around grapheme-aware editing so movement, deletion, and diagnostics avoid byte-index assumptions.
Route window switching and layout mutations through editor helpers so active-window state is saved and reloaded consistently.

Add coverage that verifies split window navigation preserves each window cursor independently.
Extract cursor terminal-position calculation so it can be tested without terminal output.

Use the active window buffer line directly when computing display columns, avoiding double application of `vtop` after scrolling.
Use the same terminal cursor position for overlay avoid-cursor placement that rendering uses for the actual cursor.

Mark overlays dirty when recalculated placement changes so cursor-driven overlays redraw after moving across screen regions.
Treat render-buffer text writes as terminal cell writes so wide characters reserve their continuation cell.

Tighten boundary checks to ignore `x == width` and `y == height` writes instead of allowing them to spill into later cells.
Measure plugin overlay width and right alignment in terminal display columns so wide characters align correctly.

Clear the full overlay area before rendering each row to prevent stale cells when content shrinks or shifts.
Use character-aware deletion for command and search backspace instead of byte slicing.

Clear the command line before rendering prompt text so Unicode search input and narrow terminal widths do not leave stale cells or underflow padding.
Use display-width-aware, saturating layout math for statusline segments so
narrow terminals do not underflow while computing file and position areas.

Clear the row before writing statusline segments to avoid stale cells when
segments shrink or overlap.
Measure completion labels, icons, details, and documentation with terminal
display widths so popup rows stay aligned for emoji and CJK text.

Add shared helpers for fitting strings to display columns without splitting
grapheme clusters during truncation.
Keep picker lists safe when filtering leaves no matches, and make list row
truncation use terminal display widths for wide Unicode labels.

Use character-aware backspace and display-width cursor math for picker search
text so non-ASCII search terms do not panic or misplace the cursor.
Draw info tooltip text from the dialog content origin instead of applying the
horizontal offset twice, which misplaced content away from its border.

Measure and pad tooltip rows by display width so wide Unicode text is clipped
and filled consistently with other popup surfaces.
Use display-width-aware title measurement and truncation when drawing dialog
borders, so long titles cannot underflow the centering calculation.

This also keeps Unicode titles aligned with the rest of the popup rendering
surfaces that measure terminal columns instead of bytes.
Use terminal display width instead of byte length when reporting the cursor
position for command and search prompts.

This keeps the cursor aligned with rendered commandline text when prompts
contain wide Unicode characters such as emoji or CJK text.
Use saturating geometry for picker dialog, list, separator, and cursor rows so
small terminal heights cannot underflow popup layout calculations.

Make editor viewport height saturating as well, matching the window manager's
reserved status and command line handling on very small screens.
Move info tooltip placement into a small geometry helper that uses saturating
math for cursor offsets, horizontal fitting, and available height checks.

This prevents hover/info popup layout from underflowing on tiny terminal
heights after viewport height is clamped to zero.
Skip statusline and commandline drawing when terminal dimensions leave no valid
row for those chrome elements, and use saturating command/search cursor rows.

This prevents refresh rendering from underflowing on one-row or zero-size test
terminal layouts.
Keep split membership checks from mutating the traversal index before recursing
into nested split trees, so resize commands can reach the active window's
inner parent split.

Also report a no-op when resizing a single window, since there is no parent
split ratio to adjust.
Advance the window traversal index when removing the target leaf so later
siblings are not mistaken for the same active window during close.

This lets closing the first window in a split collapse to the remaining sibling
instead of failing to remove the active window.
Build diagnostic rows with display-width-aware fitting so indicators and
messages cannot spill beyond the available window columns.

Preserve truncation with an ellipsis for wide Unicode diagnostics and keep
cramped rows bounded when many diagnostics share a line.
Add a bounded completion popup layout path that clamps popup width, flips above
the cursor when there is not enough space below, and limits rendered rows to
the available terminal height.

Use terminal cursor coordinates for LSP completion popups so placement includes
gutter and window offsets instead of buffer-local cursor positions.
Add configurable language server definitions and route editor LSP
requests through a manager keyed by document language and workspace root.

Keep Rust enabled by default while moving `rust-analyzer` details into
server config so other stdio LSPs can be added through `config.toml`.
Add a plugin-owned side panel surface with focus routing, layout
reservation, filesystem listing, and directory watch APIs so plugins can
build persistent tree-style UI.

Register a sample `NeoTree` plugin on `Ctrl-e`, including toggle behavior,
file opening, and explicit editor focus handoff after file activation.
Clamp viewport and cursor state after movement actions so commands that
overshoot the buffer cannot land past the final navigable line.

Cover `G`, oversized line jumps, direct cursor positioning, paging, and
scrolling against files that end with a trailing newline.
Keep `w` and `b` from scrolling when their destination line is already
visible. Word movement should update the cursor within the current viewport
and only scroll when the target word is offscreen.

Add a movement regression that asserts both next-word and previous-word
motions preserve `vtop` for visible targets.
Allow page movement to use partial pages at file edges so `Ctrl-b` and
`Ctrl-f` land on the top or final navigable line instead of refusing to
move when a full page is unavailable.

Fix visual selection rendering to include the final visible character on
selected lines and resolve selection line lengths against buffer lines.
Move `$` to the final visible character instead of one cell past the end
of the line, while keeping append-at-end behavior by moving right before
entering insert mode for `A`.

Bind `$` and `End` in visual modes so visual selections can extend to the
line end, and update movement and append tests for the new cursor behavior.
When leaving insert mode, move a one-past-end cursor back onto the final
visible character so normal mode does not remain past the line end.

Keep insert mode free to use one-past-end positions while editing and add a
regression for escaping from an append-at-end cursor.
Add buffer-local transaction undo and redo backed by text edit ranges.
Route editor and plugin mutations through the transaction path so insert
sessions, deletes, visual edits, paste, indent, and redo share one model.

Track clean revisions in the undo history so the dirty indicator clears
when undo returns a buffer to its saved state and updates after save.
Derive the default panel side and use key-based reverse sorting for picker
matches to satisfy the current Clippy toolchain. Update `bytes` in
`Cargo.lock` to the patched release required by the security audit.
@fcoury fcoury changed the title Improve editor rendering, plugins, LSP, and undo Harden editor rendering, plugins, LSP, and undo May 28, 2026
fcoury added 8 commits May 28, 2026 01:32
Use each window's buffer to calculate gutter width during window-local
rendering and mouse coordinate translation. This prevents line numbers in
inactive splits from shifting when focus moves between buffers with
different line counts.

Add a regression test that renders a split with a one-line active buffer
and a ten-line inactive buffer, then verifies line 10 is already padded.
Handle `Ctrl-j` and `Ctrl-k` inside the shared picker component so file
finder and plugin pickers can move selection without leaving the filter
input.

Add picker-level tests for control navigation and preserve plain `j` as
normal filter text.
Use the buffer navigable line count for cursor bounds, gutter rendering, and viewport rendering so Ropey synthetic trailing lines are not exposed.

Preserve unterminated final lines by filling only the remaining row after rendering their content.
Keep an opened trailing line navigable while insert mode is active so `o` at EOF keeps the cursor on the new line.

Convert visual selections from grapheme positions to Rope character positions before deleting text.
Join lines when backspace is pressed at column zero on a later line, preserving the expected insert-mode editing behavior.

Convert tab insertion from grapheme cursor coordinates to Rope character coordinates before editing multi-codepoint graphemes.
Convert word command cursor positions between grapheme and Rope character offsets before using buffer word helpers. This keeps ZWJ and combining graphemes intact during word motion and deletion.

Store full grapheme text in render cells and print that text during diff rendering so UI strings preserve multi-codepoint grapheme clusters.
Clamp panel scroll when row updates shrink the visible model so panel rendering cannot skip past the remaining rows.

Keep direct line-open insert actions in the active insert transaction so typing after `InsertLineBelowCursor` or `InsertLineAtCursor` undoes as one insert session.
Reserve both left and right plugin panel widths before laying out editor windows so right-side panels do not draw over editor content.

Commit active insert transactions before save checkpoints are marked clean, then reopen insert transactions so continued insert-mode edits keep normal grouping.
@fcoury fcoury merged commit 2db0adc into master May 28, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant