pmacs/builtin/runtime/linewrap.lua

82 lines
4.0 KiB
Lua

-- Long lines (QoL Stage 3, framing docs/long-lines-framing.md).
--
-- Declares `ui.line-wrap`. Everything that honors it is Rust: the grid
-- renderer walks it through `Viewport`, the coordinate mapping takes it
-- through `LayoutCtx`, and semantic frontends are told over
-- `InstanceMessage::LineWrapFacts` at protocol v22 because they lay out
-- locally and would otherwise never hear it.
--
-- Buffer-local (Q#LL2). The registry already supports a per-buffer
-- layer, and this is a property of the content: prose wants wrapping,
-- a log file usually does not. The *anchor* the viewport scrolls to
-- stays per-window, because two panes on one buffer scroll
-- independently.
--
-- `ui.`, not `editing.`: `editing.*` is buffer-editing behavior
-- (auto-pair, trim-on-save, line endings) and this changes only how
-- text is shown. The two existing `ui.*` settings carry a `gpu-`
-- prefix to mark frontend-specific ones, so the ABSENCE of a prefix
-- here is what says "both frontends".
pmacs.config.define {
name = "ui.line-wrap",
-- The description names what `truncate` COSTS, because the toggle's
-- status message is not enough: a user who sets this in `init.lua`
-- never invokes the toggle and so never sees it. #221 shipped that
-- gap (framing §6).
--
-- In the TUI the cost is now only the GUI's, because Stage 4 gave the
-- grid renderer horizontal scrolling. Stage 5 closes the rest, and
-- this sentence shrinks back when it lands.
description = "How a line wider than the window is shown: wrap onto following rows, or truncate at the edge. Truncated text is reachable by moving the cursor past the edge in the terminal UI; in the GUI it is not yet reachable at all.",
-- A closed set, so an unknown value is impossible rather than
-- handled. Adding "word" later is a clean additive change --- which
-- is the plan, since character wrap is what both frontends can do
-- identically today (Q#LL5) and word wrap is a deliberate future
-- choice rather than an inherited library default.
type = "enum",
choices = { "wrap", "truncate" },
-- `wrap` is the only value that leaves every character reachable
-- with this stage's machinery. It is also what the GPU already did,
-- so the default is not a behavior change there --- but it IS one in
-- the TUI, which truncated. No default can preserve both, because
-- the two frontends disagreed before this setting existed; that is
-- the defect, not a side effect of fixing it.
default = "wrap",
mutability = "live",
}
-- `truncate` leaves text past the right edge UNREACHABLE until Stage 4
-- adds horizontal scrolling. That is stated in the description above
-- rather than left for a user to discover, and it is why `truncate` is
-- not the default despite being the TUI's historical behavior.
-- Buffer-local on BOTH sides, and the pairing matters.
--
-- `pmacs.config.get(name)` reads the global chain and
-- `pmacs.config.set(name, ...)` writes the global layer, so a toggle
-- built from those two would be wrong in the case the setting exists
-- for: a buffer pinned to `truncate` would report the GLOBAL value,
-- flip the GLOBAL value, and leave that buffer exactly as it was ---
-- while silently changing every buffer that had not been pinned.
--
-- So: read with `get(name, buf)`, write with `set_local(buf, ...)`,
-- and resolve the buffer ONCE for both. Resolving twice would be a
-- narrower version of the same bug, since the active buffer can change
-- between two calls.
pmacs.command.define {
name = "ui.toggle-line-wrap",
description = "Toggle line wrapping for the current buffer",
fn = function()
local buf = pmacs.window.buffer()
local current = pmacs.config.get("ui.line-wrap", buf)
local next_mode = current == "wrap" and "truncate" or "wrap"
pmacs.config.set_local(buf, "ui.line-wrap", next_mode)
if next_mode == "truncate" then
pmacs.editor.set_status("line wrap off — move the cursor past the edge to scroll (GUI: not yet)")
else
pmacs.editor.set_status("line wrap on")
end
end,
}