diff --git a/doc/bullets.txt b/doc/bullets.txt index 3d7a1bc..1dadf33 100644 --- a/doc/bullets.txt +++ b/doc/bullets.txt @@ -83,6 +83,11 @@ GENERAL COMMANDS *bullets-commands* A blank line before/after the first/last bullet denotes the end of the list. + *bullets-:RecomputeCheckboxes* +:RecomputeCheckboxes Recomputes all partial checkboxes in the current list. + Preserves state for all checkboxes with no children and + recomputes all checkboxes up to the top of the list. + *bullets-:BulletDemote* :BulletDemote Demotes the current bullet by indenting it and changing its bullet type to the next level defined in diff --git a/plugin/bullets.vim b/plugin/bullets.vim index 839f33e..c2ec388 100644 --- a/plugin/bullets.vim +++ b/plugin/bullets.vim @@ -779,6 +779,85 @@ fun! s:set_child_checkboxes(lnum, checked) endif endfun +" Recompute partial checkboxes of a full checkbox tree given the root lnum +fun! s:recompute_checkbox_tree(lnum) + if !g:bullets_nested_checkboxes + return + endif + + let l:indent = indent(a:lnum) + let l:bullet = s:closest_bullet_types(a:lnum, l:indent) + let l:bullet = s:resolve_bullet_type(l:bullet) + + if l:bullet.bullet_type !=# 'chk' + return + endif + + " recursively recompute checkbox tree for all children, then finally self + + let l:children = s:get_children_line_numbers(a:lnum) + for l:child_nr in l:children + " nb: this skips 'grandchildren' checkboxes (i.e., children who aren't + " checkboxes but have checkbox children themselves), but those grandkids + " will be targeted by s:recompute_checkboxes_in_range anyway + call s:recompute_checkbox_tree(l:child_nr) + endfor + + + if empty(l:children) + " if no children, preserve previous checked state + " partially completed checkboxes become unchecked + if empty(l:bullet) || !has_key(l:bullet, 'checkbox_marker') + return + endif + + let l:checkbox_markers = split(g:bullets_checkbox_markers, '\zs') + let l:partial_markers = join(l:checkbox_markers[1:-2], '') + + if l:bullet.checkbox_marker =~# '\v[' . l:partial_markers . ']' + call s:set_checkbox(a:lnum, l:checkbox_markers[0]) + endif + else + " if children exist, recompute this checkbox status + let l:first_child = l:children[0] + let l:completion_marker = s:sibling_checkbox_status(l:first_child) + call s:set_checkbox(a:lnum, l:completion_marker) + endif +endfun + +fun! s:recompute_checkboxes_in_range(start, end) + if !g:bullets_nested_checkboxes + return + endif + + call s:enable_bullet_cache() + for l:nr in range(a:start, a:end) + " find all bullets who do not have a checkbox parent + let l:parent = s:get_parent(l:nr) + if !empty(l:parent) && l:parent.bullet_type ==# 'chk' + continue + end + + call s:recompute_checkbox_tree(l:nr) + endfor + call s:disable_bullet_cache() +endfun + +" Recomputes checkboxes for the whole list containing the cursor. +fun! s:recompute_checkboxes() + if !g:bullets_nested_checkboxes + return + endif + + call s:enable_bullet_cache() + let l:first_line = s:first_bullet_line(line('.')) + let l:last_line = s:last_bullet_line(line('.')) + if l:first_line > 0 && l:last_line > 0 + call s:recompute_checkboxes_in_range(l:first_line, l:last_line) + endif + call s:disable_bullet_cache() +endfun + command! SelectCheckboxInside call select_checkbox(1) command! SelectCheckbox call select_checkbox(0) command! ToggleCheckbox call toggle_checkboxes_nested() @@ -961,6 +1040,7 @@ endfun command! -range=% RenumberSelection call renumber_selection() command! RenumberList call renumber_whole_list() +command! RecomputeCheckboxes call recompute_checkboxes() " --------------------------------------------------------- }}} @@ -1107,6 +1187,9 @@ nnoremap (bullets-renumber) :RenumberList " Toggle checkbox nnoremap (bullets-toggle-checkbox) :ToggleCheckbox +" Recompute checkbox list +nnoremap (bullets-recompute-checkboxes) :RecomputeCheckboxes + " Promote and Demote outline level inoremap (bullets-demote) :BulletDemote nnoremap (bullets-demote) :BulletDemote diff --git a/spec/checkboxes_spec.rb b/spec/checkboxes_spec.rb index 814e354..1dc5b82 100644 --- a/spec/checkboxes_spec.rb +++ b/spec/checkboxes_spec.rb @@ -248,4 +248,139 @@ TEXT end + + it 'recomputes checkboxes recursively on RecomputeCheckboxes' do + filename = "#{SecureRandom.hex(6)}.txt" + write_file(filename, <<-TEXT) + # Hello there + - [ ] EXPECTED: ¼ + - [X] checkbox leaf + - [ ] EXPECTED: CHECKED + - [ ] EXPECTED: CHECKED + - [ ] EXPECTED: CHECKED + - [X] checkbox leaf + - [X] checkbox leaf + - [X] EXPECTED: ¾ + - [X] checkbox leaf + - [X] checkbox leaf + - [X] checkbox leaf + - [ ] checkbox leaf + - [X] EXPECTED: ½ + - [ ] EXPECTED: CHECKED + - [ ] EXPECTED: CHECKED + - [X] checkbox leaf + - [½] checkbox leaf (EXPECTED: UNCHECKED) + - [½] EXPECTED: UNCHECKED + - [ ] checkbox leaf + - [½] checkbox leaf (EXPECTED: UNCHECKED) + TEXT + + vim.edit filename + vim.command 'let g:bullets_checkbox_markers=" .¼½¾X"' + vim.type '9j' + vim.command 'RecomputeCheckboxes' + vim.write + + file_contents = IO.read(filename) + + expect(file_contents).to eq normalize_string_indent(<<-TEXT) + # Hello there + - [¼] EXPECTED: ¼ + - [X] checkbox leaf + - [X] EXPECTED: CHECKED + - [X] EXPECTED: CHECKED + - [X] EXPECTED: CHECKED + - [X] checkbox leaf + - [X] checkbox leaf + - [¾] EXPECTED: ¾ + - [X] checkbox leaf + - [X] checkbox leaf + - [X] checkbox leaf + - [ ] checkbox leaf + - [½] EXPECTED: ½ + - [X] EXPECTED: CHECKED + - [X] EXPECTED: CHECKED + - [X] checkbox leaf + - [ ] checkbox leaf (EXPECTED: UNCHECKED) + - [ ] EXPECTED: UNCHECKED + - [ ] checkbox leaf + - [ ] checkbox leaf (EXPECTED: UNCHECKED) + + TEXT + end + + it 'recomputes checkboxes correctly on reindents' do + filename = "#{SecureRandom.hex(6)}.txt" + write_file(filename, <<-TEXT) + # Hello there + - [X] parent bullet + - [X] first child bullet + TEXT + + vim.edit filename + vim.command 'let g:bullets_checkbox_markers=" /X"' + vim.type 'GA' + vim.feedkeys '\' + vim.command 'RecomputeCheckboxes' + vim.write + + file_contents = IO.read(filename) + + expect(file_contents).to eq normalize_string_indent(<<-TEXT) + # Hello there + - [/] parent bullet + - [X] first child bullet + - [ ] + + TEXT + + vim.command 'let g:bullets_delete_last_bullet_if_empty = 2' + vim.feedkeys '\' + vim.command 'RecomputeCheckboxes' + vim.write + + file_contents = IO.read(filename) + + expect(file_contents).to eq normalize_string_indent(<<-TEXT) + # Hello there + - [X] parent bullet + - [X] first child bullet + - [ ] + + TEXT + end + + it 'handles skip-level checkbox trees' do + filename = "#{SecureRandom.hex(6)}.txt" + write_file(filename, <<-TEXT) + # Hello there + - [X] parent bullet (EXPECTED: /) + - skip: not checkbox content + - [ ] new root bullet (EXPECTED: /) + - [ ] first child bullet + - [X] first child bullet + - [X] first child bullet + - [ ] first child bullet + TEXT + + vim.edit filename + vim.command 'let g:bullets_checkbox_markers=" /X"' + vim.type '2j' + vim.command 'RecomputeCheckboxes' + vim.write + + file_contents = IO.read(filename) + + expect(file_contents).to eq normalize_string_indent(<<-TEXT) + # Hello there + - [/] parent bullet (EXPECTED: /) + - skip: not checkbox content + - [/] new root bullet (EXPECTED: /) + - [ ] first child bullet + - [X] first child bullet + - [X] first child bullet + - [ ] first child bullet + + TEXT + end end