124 lines
4.3 KiB
Lua
124 lines
4.3 KiB
Lua
-- pmacs-outline/view.lua --- Visible projection rendering (T M8.9).
|
|
--
|
|
-- Builds a read-only projection buffer from a source outline buffer
|
|
-- + per-handle fold state. Folding visually replaces a folded
|
|
-- entry's body bytes with a `...` marker; selective rendering
|
|
-- derives entirely from the parsed structure.
|
|
--
|
|
-- The projection is *byte-substitution*, not a structural rebuild:
|
|
-- for each top-level folded subtree (folded entries that are not
|
|
-- inside another folded ancestor's hidden range), the source bytes
|
|
-- from `headline_byte_end` to `byte_end` are replaced with the
|
|
-- collapse marker; everything outside those ranges appears
|
|
-- verbatim. This keeps the projection text close to the source
|
|
-- text, so cursor navigation in the visible buffer maps back to
|
|
-- source byte ranges via the line index built during render.
|
|
|
|
local M = {}
|
|
|
|
-- Top-level folded subtrees: walk entries in DFS order, skip any
|
|
-- entry whose ancestor (in byte-range terms) is already folded.
|
|
-- Each emitted range is `{ headline_byte_end, byte_end }` of the
|
|
-- folded entry.
|
|
local function top_level_folded(entries, fold_state)
|
|
local out = {}
|
|
local active_end = -1 -- byte_end of the outermost active fold
|
|
for _, e in ipairs(entries) do
|
|
if e.byte_start < active_end then
|
|
-- Inside an already-folded ancestor: skip.
|
|
else
|
|
active_end = -1
|
|
if fold_state[e.byte_start] then
|
|
out[#out + 1] = { e.headline_byte_end, e.byte_end, e }
|
|
active_end = e.byte_end
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Build the visible text + a line_to_byte map (1-indexed line
|
|
-- numbers in the visible buffer -> byte offsets in the source). The
|
|
-- map only records the byte offset of the *start* of each visible
|
|
-- line; intermediate columns are not tracked.
|
|
function M.render(source_text, entries, fold_state, marker)
|
|
marker = marker or " ...\n"
|
|
local folds = top_level_folded(entries, fold_state)
|
|
table.sort(folds, function(a, b) return a[1] < b[1] end)
|
|
|
|
local parts = {}
|
|
local cursor = 0 -- 0-indexed byte cursor into source_text
|
|
for _, f in ipairs(folds) do
|
|
local fold_start, fold_end = f[1], f[2]
|
|
if fold_start > cursor then
|
|
parts[#parts + 1] = source_text:sub(cursor + 1, fold_start)
|
|
end
|
|
parts[#parts + 1] = marker
|
|
cursor = fold_end
|
|
end
|
|
if cursor < #source_text then
|
|
parts[#parts + 1] = source_text:sub(cursor + 1)
|
|
end
|
|
local text = table.concat(parts)
|
|
|
|
-- visible_to_source: per visible line, the source byte offset of
|
|
-- that line's first character. Built by walking visible text and
|
|
-- tracking corresponding source offsets through the substitutions.
|
|
local visible_to_source = { 0 }
|
|
local src_idx = 0
|
|
local fold_idx = 1
|
|
local i = 1
|
|
while i <= #text do
|
|
local nl = text:find("\n", i, true)
|
|
if not nl then break end
|
|
-- Advance src_idx by the same number of bytes from `i` to `nl`,
|
|
-- but jump source offsets across folded ranges.
|
|
local visible_consumed = nl - i + 1
|
|
local k = i
|
|
while k <= nl do
|
|
-- If we're at the start of the marker, jump source past the fold.
|
|
if fold_idx <= #folds and src_idx == folds[fold_idx][1]
|
|
and text:sub(k, k + #marker - 1) == marker then
|
|
src_idx = folds[fold_idx][2]
|
|
fold_idx = fold_idx + 1
|
|
k = k + #marker
|
|
else
|
|
src_idx = src_idx + 1
|
|
k = k + 1
|
|
end
|
|
end
|
|
visible_to_source[#visible_to_source + 1] = src_idx
|
|
i = nl + 1
|
|
end
|
|
|
|
return {
|
|
text = text,
|
|
visible_to_source = visible_to_source,
|
|
folds = folds,
|
|
}
|
|
end
|
|
|
|
-- Find the byte offset in the *source* buffer corresponding to a
|
|
-- visible-buffer line number (0-indexed, matching pmacs's
|
|
-- cursor_line()). Out-of-range lines map to source EOF.
|
|
function M.source_byte_at_visible_line(projection, line_0indexed)
|
|
local map = projection.visible_to_source
|
|
local idx = line_0indexed + 1 -- map is 1-indexed
|
|
if idx < 1 then return 0 end
|
|
if idx > #map then return map[#map] end
|
|
return map[idx]
|
|
end
|
|
|
|
-- Inverse: given a source byte offset, find the visible line that
|
|
-- shows it (or the line of the fold marker that hides it).
|
|
function M.visible_line_at_source_byte(projection, source_byte)
|
|
local map = projection.visible_to_source
|
|
-- Walk forward until map[i+1] > source_byte; line is i-1 (0-indexed).
|
|
for i = 1, #map do
|
|
if map[i] > source_byte then return i - 2 end
|
|
end
|
|
return #map - 1
|
|
end
|
|
|
|
return M
|