Files
neovim/test/functional/treesitter/fold_spec.lua
zeertzjq d2dad30898 vim-patch:9.1.1439: Last diff folds not merged (#34380)
Problem:  Last diff folds not merged (after v8.1.1922)
Solution: loop over all windows in the current tabpage and update all
          folds (Gary Johnson)

This commit fixes a bug where the last two folds of a diff are not
merged when the last difference between the two diff'd buffers is
resolved.

Normally, when two buffers are diff'd, folding is used to show only the
text that differs and to hide the text that is the same between the two
buffers.  When a difference is resolved by making a block of text the
same in both buffers, the folds are updated to merge that block with the
folds above and below it into one closed fold.

That updating of the folds did not occur when the block of text was the
last diff block in the buffers.

The bug was introduced by this patch on August 24, 2019:

    patch 8.1.1922: in diff mode global operations can be very slow

    Problem:    In diff mode global operations can be very slow.
    Solution:   Do not call diff_redraw() many times, call it once when
		redrawing.  And also don't update folds multiple times.

Unfortunately, folds were then not updated often enough.

The problem was fixed by adding a short loop to the ex_diffgetput()
function in diff.c to update all the folds in the current tab when the
last difference is removed.

A test for this was added to test_diffmode.vim.  Two of the reference
screen dumps for another test in that file,
Test_diffget_diffput_linematch(), had to be changed to have all the
folds closed rather than to have the last diff block remain open.

closes: vim/vim#17457

3fa0d3514b

Co-authored-by: Gary Johnson <garyjohn@spocom.com>
2025-06-09 06:57:54 +08:00

834 lines
25 KiB
Lua

local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local clear = n.clear
local eq = t.eq
local insert = n.insert
local write_file = t.write_file
local exec_lua = n.exec_lua
local command = n.command
local feed = n.feed
local poke_eventloop = n.poke_eventloop
before_each(clear)
describe('treesitter foldexpr', function()
clear()
before_each(function()
-- open folds to avoid deleting entire folded region
exec_lua([[vim.opt.foldlevel = 9]])
end)
local test_text = [[
void ui_refresh(void)
{
int width = INT_MAX, height = INT_MAX;
bool ext_widgets[kUIExtCount];
for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
ext_widgets[i] = true;
}
bool inclusive = ui_override();
for (size_t i = 0; i < ui_count; i++) {
UI *ui = uis[i];
width = MIN(ui->width, width);
height = MIN(ui->height, height);
foo = BAR(ui->bazaar, bazaar);
for (UIExtension j = 0; (int)j < kUIExtCount; j++) {
ext_widgets[j] &= (ui->ui_ext[j] || inclusive);
}
}
}]]
local function parse(lang)
exec_lua(
([[vim.treesitter.get_parser(0, %s):parse()]]):format(lang and '"' .. lang .. '"' or 'nil')
)
end
local function get_fold_levels()
return exec_lua(function()
local res = {}
for i = 1, vim.api.nvim_buf_line_count(0) do
res[i] = vim.treesitter.foldexpr(i)
end
return res
end)
end
it('can compute fold levels', function()
insert(test_text)
parse('c')
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
[4] = '1',
[5] = '>2',
[6] = '2',
[7] = '2',
[8] = '1',
[9] = '1',
[10] = '>2',
[11] = '2',
[12] = '2',
[13] = '2',
[14] = '2',
[15] = '>3',
[16] = '3',
[17] = '3',
[18] = '2',
[19] = '1',
}, get_fold_levels())
end)
it('recomputes fold levels after lines are added/removed', function()
insert(test_text)
parse('c')
command('1,2d')
poke_eventloop()
eq({
[1] = '0',
[2] = '0',
[3] = '>1',
[4] = '1',
[5] = '1',
[6] = '0',
[7] = '0',
[8] = '>1',
[9] = '1',
[10] = '1',
[11] = '1',
[12] = '1',
[13] = '>2',
[14] = '2',
[15] = '2',
[16] = '1',
[17] = '0',
}, get_fold_levels())
command('1put!')
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
[4] = '1',
[5] = '>2',
[6] = '2',
[7] = '2',
[8] = '1',
[9] = '1',
[10] = '>2',
[11] = '2',
[12] = '2',
[13] = '2',
[14] = '2',
[15] = '>3',
[16] = '3',
[17] = '3',
[18] = '2',
[19] = '1',
}, get_fold_levels())
end)
it('handles changes close to start/end of folds', function()
insert([[
# h1
t1
# h2
t2]])
exec_lua([[vim.treesitter.query.set('markdown', 'folds', '(section) @fold')]])
parse('markdown')
eq({
[1] = '>1',
[2] = '1',
[3] = '>1',
[4] = '1',
}, get_fold_levels())
feed('2ggo<Esc>')
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
[4] = '>1',
[5] = '1',
}, get_fold_levels())
feed('dd')
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '>1',
[4] = '1',
}, get_fold_levels())
feed('2ggdd')
poke_eventloop()
eq({
[1] = '0',
[2] = '>1',
[3] = '1',
}, get_fold_levels())
feed('u')
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '>1',
[4] = '1',
}, get_fold_levels())
feed('3ggdd')
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
}, get_fold_levels())
feed('u')
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '>1',
[4] = '1',
}, get_fold_levels())
feed('3ggI#<Esc>')
parse()
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '>2',
[4] = '2',
}, get_fold_levels())
feed('x')
parse()
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '>1',
[4] = '1',
}, get_fold_levels())
end)
it('handles changes that trigger multiple on_bytes', function()
insert([[
function f()
asdf()
asdf()
end
-- comment]])
exec_lua(function()
vim.treesitter.query.set(
'lua',
'folds',
'[(function_declaration) (parameters) (arguments)] @fold'
)
end)
parse('lua')
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
[4] = '1',
[5] = '0',
}, get_fold_levels())
command('1,4join')
poke_eventloop()
eq({
[1] = '0',
[2] = '0',
}, get_fold_levels())
feed('u')
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
[4] = '1',
[5] = '0',
}, get_fold_levels())
end)
it('handles multiple folds that overlap at the end and start', function()
insert([[
function f()
g(
function()
asdf()
end, function()
end
)
end]])
exec_lua(function()
vim.treesitter.query.set(
'lua',
'folds',
'[(function_declaration) (function_definition) (parameters) (arguments)] @fold'
)
end)
parse('lua')
-- If fold1.stop = fold2.start, then move fold1's stop up so that fold2.start gets proper level.
eq({
[1] = '>1',
[2] = '>2',
[3] = '>3',
[4] = '3',
[5] = '>3',
[6] = '3',
[7] = '2',
[8] = '1',
}, get_fold_levels())
command('1,8join')
feed('u')
poke_eventloop()
eq({
[1] = '>1',
[2] = '>2',
[3] = '>3',
[4] = '3',
[5] = '>3',
[6] = '3',
[7] = '2',
[8] = '1',
}, get_fold_levels())
end)
it('handles multiple folds that start at the same line', function()
insert([[
function f(a)
if #(g({
k = v,
})) > 0 then
return
end
end]])
exec_lua(function()
vim.treesitter.query.set(
'lua',
'folds',
'[(if_statement) (function_declaration) (parameters) (arguments) (table_constructor)] @fold'
)
end)
parse('lua')
eq({
[1] = '>1',
[2] = '>3',
[3] = '3',
[4] = '3',
[5] = '2',
[6] = '2',
[7] = '1',
}, get_fold_levels())
command('2,6join')
poke_eventloop()
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
}, get_fold_levels())
feed('u')
poke_eventloop()
eq({
[1] = '>1',
[2] = '>3',
[3] = '3',
[4] = '3',
[5] = '2',
[6] = '2',
[7] = '1',
}, get_fold_levels())
end)
it('takes account of relevant options', function()
insert([[
# h1
t1
## h2
t2
### h3
t3]])
exec_lua([[vim.treesitter.query.set('markdown', 'folds', '(section) @fold')]])
parse('markdown')
command([[set foldminlines=2]])
eq({
[1] = '>1',
[2] = '1',
[3] = '>2',
[4] = '2',
[5] = '2',
[6] = '2',
}, get_fold_levels())
command([[set foldminlines=1 foldnestmax=1]])
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
[4] = '1',
[5] = '1',
[6] = '1',
}, get_fold_levels())
end)
it('handles quantified patterns', function()
insert([[
-- hello
-- hello
-- hello
-- hello
-- hello
-- hello]])
exec_lua([[vim.treesitter.query.set('lua', 'folds', '(comment)+ @fold')]])
parse('lua')
eq({
[1] = '>1',
[2] = '1',
[3] = '1',
[4] = '1',
[5] = '1',
[6] = '1',
}, get_fold_levels())
end)
it('updates folds in all windows', function()
local screen = Screen.new(60, 48)
screen:set_default_attr_ids({
[1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue },
[2] = { bold = true, foreground = Screen.colors.Blue1 },
[3] = { bold = true, reverse = true },
[4] = { reverse = true },
})
parse('c')
command([[set foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr() foldcolumn=1]])
command('split')
insert(test_text)
screen:expect {
grid = [[
{1:-}void ui_refresh(void) |
{1:│}{ |
{1:│} int width = INT_MAX, height = INT_MAX; |
{1:│} bool ext_widgets[kUIExtCount]; |
{1:-} for (UIExtension i = 0; (int)i < kUIExtCount; i++) { |
{1:2} ext_widgets[i] = true; |
{1:2} } |
{1:│} |
{1:│} bool inclusive = ui_override(); |
{1:-} for (size_t i = 0; i < ui_count; i++) { |
{1:2} UI *ui = uis[i]; |
{1:2} width = MIN(ui->width, width); |
{1:2} height = MIN(ui->height, height); |
{1:2} foo = BAR(ui->bazaar, bazaar); |
{1:-} for (UIExtension j = 0; (int)j < kUIExtCount; j++) { |
{1:3} ext_widgets[j] &= (ui->ui_ext[j] || inclusive); |
{1:3} } |
{1:2} } |
{1:│}^} |
{2:~ }|*4
{3:[No Name] [+] }|
{1:-}void ui_refresh(void) |
{1:│}{ |
{1:│} int width = INT_MAX, height = INT_MAX; |
{1:│} bool ext_widgets[kUIExtCount]; |
{1:-} for (UIExtension i = 0; (int)i < kUIExtCount; i++) { |
{1:2} ext_widgets[i] = true; |
{1:2} } |
{1:│} |
{1:│} bool inclusive = ui_override(); |
{1:-} for (size_t i = 0; i < ui_count; i++) { |
{1:2} UI *ui = uis[i]; |
{1:2} width = MIN(ui->width, width); |
{1:2} height = MIN(ui->height, height); |
{1:2} foo = BAR(ui->bazaar, bazaar); |
{1:-} for (UIExtension j = 0; (int)j < kUIExtCount; j++) { |
{1:3} ext_widgets[j] &= (ui->ui_ext[j] || inclusive); |
{1:3} } |
{1:2} } |
{1:│}} |
{2:~ }|*3
{4:[No Name] [+] }|
|
]],
}
command('1,2d')
screen:expect {
grid = [[
{1: } ^int width = INT_MAX, height = INT_MAX; |
{1: } bool ext_widgets[kUIExtCount]; |
{1:-} for (UIExtension i = 0; (int)i < kUIExtCount; i++) { |
{1:│} ext_widgets[i] = true; |
{1:│} } |
{1: } |
{1: } bool inclusive = ui_override(); |
{1:-} for (size_t i = 0; i < ui_count; i++) { |
{1:│} UI *ui = uis[i]; |
{1:│} width = MIN(ui->width, width); |
{1:│} height = MIN(ui->height, height); |
{1:│} foo = BAR(ui->bazaar, bazaar); |
{1:-} for (UIExtension j = 0; (int)j < kUIExtCount; j++) { |
{1:2} ext_widgets[j] &= (ui->ui_ext[j] || inclusive); |
{1:2} } |
{1:│} } |
{1: }} |
{2:~ }|*6
{3:[No Name] [+] }|
{1: } int width = INT_MAX, height = INT_MAX; |
{1: } bool ext_widgets[kUIExtCount]; |
{1:-} for (UIExtension i = 0; (int)i < kUIExtCount; i++) { |
{1:│} ext_widgets[i] = true; |
{1:│} } |
{1: } |
{1: } bool inclusive = ui_override(); |
{1:-} for (size_t i = 0; i < ui_count; i++) { |
{1:│} UI *ui = uis[i]; |
{1:│} width = MIN(ui->width, width); |
{1:│} height = MIN(ui->height, height); |
{1:│} foo = BAR(ui->bazaar, bazaar); |
{1:-} for (UIExtension j = 0; (int)j < kUIExtCount; j++) { |
{1:2} ext_widgets[j] &= (ui->ui_ext[j] || inclusive); |
{1:2} } |
{1:│} } |
{1: }} |
{2:~ }|*5
{4:[No Name] [+] }|
|
]],
}
feed([[O<C-u><C-r>"<BS><Esc>]])
screen:expect {
grid = [[
{1:-}void ui_refresh(void) |
{1:│}^{ |
{1:│} int width = INT_MAX, height = INT_MAX; |
{1:│} bool ext_widgets[kUIExtCount]; |
{1:-} for (UIExtension i = 0; (int)i < kUIExtCount; i++) { |
{1:2} ext_widgets[i] = true; |
{1:2} } |
{1:│} |
{1:│} bool inclusive = ui_override(); |
{1:-} for (size_t i = 0; i < ui_count; i++) { |
{1:2} UI *ui = uis[i]; |
{1:2} width = MIN(ui->width, width); |
{1:2} height = MIN(ui->height, height); |
{1:2} foo = BAR(ui->bazaar, bazaar); |
{1:-} for (UIExtension j = 0; (int)j < kUIExtCount; j++) { |
{1:3} ext_widgets[j] &= (ui->ui_ext[j] || inclusive); |
{1:3} } |
{1:2} } |
{1:│}} |
{2:~ }|*4
{3:[No Name] [+] }|
{1:-}void ui_refresh(void) |
{1:│}{ |
{1:│} int width = INT_MAX, height = INT_MAX; |
{1:│} bool ext_widgets[kUIExtCount]; |
{1:-} for (UIExtension i = 0; (int)i < kUIExtCount; i++) { |
{1:2} ext_widgets[i] = true; |
{1:2} } |
{1:│} |
{1:│} bool inclusive = ui_override(); |
{1:-} for (size_t i = 0; i < ui_count; i++) { |
{1:2} UI *ui = uis[i]; |
{1:2} width = MIN(ui->width, width); |
{1:2} height = MIN(ui->height, height); |
{1:2} foo = BAR(ui->bazaar, bazaar); |
{1:-} for (UIExtension j = 0; (int)j < kUIExtCount; j++) { |
{1:3} ext_widgets[j] &= (ui->ui_ext[j] || inclusive); |
{1:3} } |
{1:2} } |
{1:│}} |
{2:~ }|*3
{4:[No Name] [+] }|
|
]],
}
end)
it("doesn't open folds in diff mode", function()
local screen = Screen.new(60, 36)
parse('c')
command(
[[set foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr() foldcolumn=1 foldlevel=9]]
)
insert(test_text)
command('16d')
command('new')
insert(test_text)
command('windo diffthis')
feed('do')
screen:expect([[
{7:+ }{13:+-- 19 lines: void ui_refresh(void)·······················}|
{1:~ }|*16
{2:[No Name] [+] }|
{7:+ }{13:^+-- 19 lines: void ui_refresh(void)·······················}|
{1:~ }|*15
{3:[No Name] [+] }|
|
]])
end)
it('does not extend closed fold with `o`/`O`', function()
local screen = Screen.new(60, 24)
insert(test_text)
parse('c')
command([[set foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr() foldcolumn=1]])
feed('5ggzco')
screen:expect({
grid = [[
{7:-}void ui_refresh(void) |
{7:│}{ |
{7:│} int width = INT_MAX, height = INT_MAX; |
{7:│} bool ext_widgets[kUIExtCount]; |
{7:+}{13:+--- 3 lines: for (UIExtension i = 0; (int)i < kUIExtCount}|
{7:│}^ |
{7:│} |
{7:│} bool inclusive = ui_override(); |
{7:-} for (size_t i = 0; i < ui_count; i++) { |
{7:2} UI *ui = uis[i]; |
{7:2} width = MIN(ui->width, width); |
{7:2} height = MIN(ui->height, height); |
{7:2} foo = BAR(ui->bazaar, bazaar); |
{7:-} for (UIExtension j = 0; (int)j < kUIExtCount; j++) { |
{7:3} ext_widgets[j] &= (ui->ui_ext[j] || inclusive); |
{7:3} } |
{7:2} } |
{7:│}} |
{1:~ }|*5
{5:-- INSERT --} |
]],
})
feed('<Esc>O')
screen:expect({
grid = [[
{7:-}void ui_refresh(void) |
{7:│}{ |
{7:│} int width = INT_MAX, height = INT_MAX; |
{7:│} bool ext_widgets[kUIExtCount]; |
{7:+}{13:+--- 3 lines: for (UIExtension i = 0; (int)i < kUIExtCount}|
{7:│}^ |
{7:│} |*2
{7:│} bool inclusive = ui_override(); |
{7:-} for (size_t i = 0; i < ui_count; i++) { |
{7:2} UI *ui = uis[i]; |
{7:2} width = MIN(ui->width, width); |
{7:2} height = MIN(ui->height, height); |
{7:2} foo = BAR(ui->bazaar, bazaar); |
{7:-} for (UIExtension j = 0; (int)j < kUIExtCount; j++) { |
{7:3} ext_widgets[j] &= (ui->ui_ext[j] || inclusive); |
{7:3} } |
{7:2} } |
{7:│}} |
{1:~ }|*4
{5:-- INSERT --} |
]],
})
end)
it("doesn't open folds that are not touched", function()
-- test is known to be flaky
-- https://github.com/neovim/neovim/issues/33910
if t.skip_fragile(pending) then
return
end
local screen = Screen.new(40, 8)
screen:set_default_attr_ids({
[1] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.Gray },
[2] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.LightGray },
[3] = { foreground = Screen.colors.Blue1, bold = true },
[4] = { bold = true },
})
insert([[
# h1
t1
# h2
t2]])
exec_lua([[vim.treesitter.query.set('markdown', 'folds', '(section) @fold')]])
parse('markdown')
command(
[[set foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr() foldcolumn=1 foldlevel=0]]
)
feed('ggzojo')
poke_eventloop()
screen:expect {
grid = [[
{1:-}# h1 |
{1:│}t1 |
{1:-}^ |
{1:+}{2:+-- 2 lines: # h2·····················}|
{3:~ }|*3
{4:-- INSERT --} |
]],
}
feed('<Esc>u')
-- TODO(tomtomjhj): `u` spuriously opens the fold (#26499).
feed('zMggzo')
feed('dd')
poke_eventloop()
screen:expect {
grid = [[
{1:-}^t1 |
{1:-}# h2 |
{1:│}t2 |
{3:~ }|*4
1 line less; before #2 {MATCH:.*}|
]],
}
end)
it("doesn't call get_parser too often when parser is not available", function()
-- spy on vim.treesitter.get_parser() to keep track of how many times it is called
exec_lua(function()
_G.count = 0
vim.treesitter.get_parser = (function(wrapped)
return function(...)
_G.count = _G.count + 1
return wrapped(...)
end
end)(vim.treesitter.get_parser)
end)
insert(test_text)
command [[
set filetype=some_filetype_without_treesitter_parser
set foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr() foldcolumn=1 foldlevel=0
]]
-- foldexpr will return '0' for all lines
local levels = get_fold_levels() ---@type integer[]
eq(19, #levels)
for lnum, level in ipairs(levels) do
eq('0', level, string.format("foldlevel[%d] == %s; expected '0'", lnum, level))
end
eq(
1,
exec_lua [[ return _G.count ]],
'count should not be as high as the # of lines; actually only once for the buffer.'
)
end)
it('can detect a new parser and refresh folds accordingly', function()
local name = t.tmpname()
write_file(name, test_text)
command('edit ' .. name)
command [[
set filetype=some_filetype_without_treesitter_parser
set foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr() foldcolumn=1 foldlevel=0
]]
-- foldexpr will return '0' for all lines
local function expect_no_folds()
local levels = get_fold_levels() ---@type integer[]
eq(19, #levels)
for lnum, level in ipairs(levels) do
eq('0', level, string.format("foldlevel[%d] == %s; expected '0'", lnum, level))
end
end
expect_no_folds()
-- reload buffer as c filetype to simulate new parser being found
feed('GA// vim: ft=c<Esc>')
command([[write | edit]])
local foldlevels = {
[1] = '>1',
[2] = '1',
[3] = '1',
[4] = '1',
[5] = '>2',
[6] = '2',
[7] = '2',
[8] = '1',
[9] = '1',
[10] = '>2',
[11] = '2',
[12] = '2',
[13] = '2',
[14] = '2',
[15] = '>3',
[16] = '3',
[17] = '3',
[18] = '2',
[19] = '1',
}
eq(foldlevels, get_fold_levels())
-- only changing filetype should change the parser again
command('set ft=some_filetype_without_treesitter_parser')
expect_no_folds()
command('set ft=c')
eq(foldlevels, get_fold_levels())
end)
end)