Edit: I cited the wrong key: I meant <S> instead of <a-K>. The text was updated to fix the mistake.
Nested folds can be achieved using a trick: further refining a folding region if it happens to contain the main cursor. That is, if a folding region contains the cursor, Kakoune will show it unfolded. Then, since thereās no reason for it to be marked as a folding region, we can proceed to fold subregions inside of it.
Here is an implementation of the idea. Itās a bit buggy, but itās enough to prove the concept.
define-command fold-enable -params 1 -docstring %{
fold-enable <keys>: enable code folding on the current window. Folding regions
are defined by <keys> as following: given an arbitrary selection of the current
buffer, <keys> will select subregions of that selection, and that subregions
will be folded.
} %{
fold-disable
set-option window fold_keys %arg{1}
hook -group fold-update window NormalIdle .* fold-update
add-highlighter window/fold_ranges replace-ranges fold_ranges
}
define-command fold-curly-brackets %[
fold-enable 's\{<ret>m'
]
define-command fold-python %{
fold-enable 's^\s*(class|def)<ret>/:$<ret>j<a-i>i'
}
define-command -hidden fold-update %{
evaluate-commands -save-regs fg %{
# Save cursor position so that we can refine folding regions below.
set-register g %val{selection_desc}
evaluate-commands -draft %{
# Execute the provided keys to get the fold regions.
#
# It may happen that the provided keys may result in no selection, so
# we need to wrap this in a try block.
try %{
execute-keys '%' %opt{fold_keys}
# Since Kakoune always visually unfolds a region when the cursor
# is over it, it will become apparent that nested regions didn't
# get folded. So, when the cursor is over a folded region, we must
# fold the nested regions instead.
evaluate-commands -itersel %{
# Cut off the cursor from the current selection and then execute
# the folding keys on the selections left after the cut.
selection-difference %reg{g}
execute-keys %opt{fold_keys}
}
}
set-register f %val{selections_desc}
}
edit -scratch
execute-keys '"f<a-P>' a<ret><left> '|{rgb:888888+rF@Default}...' <esc> 'x_'
set-register f %val{selections}
delete-buffer
try %{
# Again, we get no selections when executing the provided keys, then
# the register `f` wil contain no range-spec. In that case, we should
# fail gracefully.
set-option window fold_ranges %val{timestamp} %reg{f}
}
}
}
# Unfortunatelly, marks don't have an operation for computing the difference, only
# union and intersection. The <S> key does that using regular expressions, but
# there's no way to do that using selections. So I implemented that functionality
# here.
define-command -hidden selection-difference -params 1 %{
evaluate-commands %{
lua %val{selection_desc} %arg{1} %{
local Vec = {}
function Vec.new(line, column)
return setmetatable({line = line, column = column}, Vec)
end
function Vec.__eq(a, b)
return a.line == b.line and a.column == b.column
end
function Vec.__lt(a, b)
return a.line < b.line or a.line == b.line and a.column < b.column
end
function Vec.__le(a, b)
return a < b or a == b
end
local selection_desc = arg[1]
local mark_desc = arg[2]
local a_line, a_col, b_line, b_col =
selection_desc:match("(%d+).(%d+),(%d+).(%d+)")
local selection = {
first = Vec.new(tonumber(a_line), tonumber(a_col)),
last = Vec.new(tonumber(b_line), tonumber(b_col)),
}
local a_line, a_col, b_line, b_col =
mark_desc:match("(%d+).(%d+),(%d+).(%d+)")
local mark = {
first = Vec.new(tonumber(a_line), tonumber(a_col)),
last = Vec.new(tonumber(b_line), tonumber(b_col)),
}
function format_selection_desc(vec1, vec2)
return string.format("%d.%d,%d.%d", vec1.line, vec1.column, vec2.line, vec2.column)
end
-- An heuristics to avoid that the user-provided keys select again the
-- whole region instead of just a subregion.
function shrink_selections()
kak.execute_keys("H<a-;>L<a-;>")
end
-- First case: no intersections
if mark.last < selection.first or selection.last < mark.first then
return
end
-- Second case: selection starts and ends before mark.
if selection.first < mark.first and selection.last <= mark.last then
kak.select(format_selection_desc(selection.first, mark.first))
shrink_selections()
return
end
-- Third case: mark starts and ends before selection.
if mark.first <= selection.first and mark.last < selection.last then
kak.select(format_selection_desc(mark.last, selection.last))
shrink_selections()
return
end
-- Four case: both are equal.
if mark.first == selection.first and mark.last == selection.last then
kak.fail("no selections remaining")
end
-- Fifth case: mark is within selection and we must split selection in two.
kak.select(
format_selection_desc(selection.first, mark.first),
format_selection_desc(mark.last, selection.last)
)
shrink_selections()
}
}
}
Some considerations:
- I made a subtle change in the meaning of the keys
fold-enable accepts as a parameter. Before, it meant: āthat keys, when executed, will create selections in the entire buffer that will be used as folding regionsā. Now it means: āgiven an arbitrary pre-defined selection, that keys, when executed, will create selections inside that pre-defined selection that will be used as folding regionsā. That changes the keys for folding curly brackets from %s\{<ret>m to s\{<ret>m (without the inital %) for example. That change in the meaning of the keys allows me to use the keys both to create the initial folding regions and to refine the region containing the cursor.
- I missed an operation for combining marks using the difference of selections (instead of unior or intersection). Thatās odd, since we can split a selection with a regular expression using the
<S> key but canāt do the same using another selection. So I implemented that functionality. For convenience, I used Luar for that, but any tool can do the job.