" vim:tabstop=2:shiftwidth=2:expandtab:textwidth=99 " Vimwiki autoload plugin file " Description: Everything concerning lists and checkboxes " Home: https://github.com/vimwiki/vimwiki/ if exists('g:loaded_vimwiki_list_auto') || &compatible finish endif let g:loaded_vimwiki_list_auto = 1 " --------------------------------------------------------- " incrementation functions for the various kinds of numbers " --------------------------------------------------------- function! s:increment_1(value) abort return eval(a:value) + 1 endfunction function! s:increment_A(value) abort let list_of_chars = split(a:value, '.\zs') let done = 0 for idx in reverse(range(len(list_of_chars))) let cur_num = char2nr(list_of_chars[idx]) if cur_num < 90 let list_of_chars[idx] = nr2char(cur_num + 1) let done = 1 break else let list_of_chars[idx] = 'A' endif endfor if !done call insert(list_of_chars, 'A') endif return join(list_of_chars, '') endfunction function! s:increment_a(value) abort let list_of_chars = split(a:value, '.\zs') let done = 0 for idx in reverse(range(len(list_of_chars))) let cur_num = char2nr(list_of_chars[idx]) if cur_num < 122 let list_of_chars[idx] = nr2char(cur_num + 1) let done = 1 break else let list_of_chars[idx] = 'a' endif endfor if !done call insert(list_of_chars, 'a') endif return join(list_of_chars, '') endfunction function! s:increment_I(value) abort let subst_list = [ ['XLVIII$', 'IL'], ['VIII$', 'IX'], ['III$', 'IV'], \ ['DCCCXCIX$', 'CM'], ['CCCXCIX$', 'CD'], ['LXXXIX$', 'XC'], \ ['XXXIX$', 'XL'], ['\(I\{1,2\}\)$', '\1I'], ['CDXCIX$', 'D'], \ ['CMXCIX$', 'M'], ['XCIX$', 'C'], ['I\([VXLCDM]\)$', '\1'], \ ['\([VXLCDM]\)$', '\1I'] ] for [regex, subst] in subst_list if a:value =~# regex return substitute(a:value, regex, subst, '') endif endfor return '' endfunction function! s:increment_i(value) abort let subst_list = [ ['xlviii$', 'il'], ['viii$', 'ix'], ['iii$', 'iv'], \ ['dcccxcix$', 'cm'], ['cccxcix$', 'cd'], ['lxxxix$', 'xc'], \ ['xxxix$', 'xl'], ['\(i\{1,2\}\)$', '\1i'], ['cdxcix$', 'd'], \ ['cmxcix$', 'm'], ['xcix$', 'c'], ['i\([vxlcdm]\)$', '\1'], \ ['\([vxlcdm]\)$', '\1i'] ] for [regex, subst] in subst_list if a:value =~# regex return substitute(a:value, regex, subst, '') endif endfor return '' endfunction " --------------------------------------------------------- " utility functions " --------------------------------------------------------- function! s:substitute_rx_in_line(lnum, pattern, new_string) abort call setline(a:lnum, substitute(getline(a:lnum), a:pattern, a:new_string, '')) endfunction function! s:substitute_string_in_line(lnum, old_string, new_string) abort call s:substitute_rx_in_line(a:lnum, vimwiki#u#escape(a:old_string), a:new_string) endfunction function! s:first_char(string) abort return matchstr(a:string, '^.') endfunction if exists('*strdisplaywidth') function! s:string_length(str) abort return strdisplaywidth(a:str) endfunction else function! s:string_length(str) abort return strlen(substitute(a:str, '.', 'x', 'g')) endfunction endif function! vimwiki#lst#default_symbol() abort return vimwiki#vars#get_syntaxlocal('list_markers')[0] endfunction function! vimwiki#lst#get_list_margin() abort let list_margin = vimwiki#vars#get_wikilocal('list_margin') if list_margin < 0 return &shiftwidth else return list_margin endif endfunction "Returns: the column where the text of a line starts (possible list item "markers and checkboxes are skipped) function! s:text_begin(lnum) abort return s:string_length(matchstr(getline(a:lnum), vimwiki#vars#get_syntaxlocal('rxListItem'))) endfunction "Returns: 2 if there is a marker and text " 1 for a marker and no text " 0 for no marker at all (empty line or only text) function! s:line_has_marker(lnum) abort if getline(a:lnum) =~# vimwiki#vars#get_syntaxlocal('rxListItem').'\s*$' return 1 elseif getline(a:lnum) =~# vimwiki#vars#get_syntaxlocal('rxListItem').'\s*\S' return 2 else return 0 endif endfunction " --------------------------------------------------------- " get properties of a list item " --------------------------------------------------------- "Returns: the mainly used data structure in this file "An item represents a single list item and is a dictionary with the keys "lnum - the line number of the list item "type - 1 for bulleted item, 2 for numbered item, 0 for a regular line "mrkr - the concrete marker, e.g. '**' or 'b)' "cb - the char in the checkbox or '' if there is no checkbox function! s:get_item(lnum) abort let item = {'lnum': a:lnum} if a:lnum == 0 || a:lnum > line('$') let item.type = 0 return item endif let matches = matchlist(getline(a:lnum), vimwiki#vars#get_syntaxlocal('rxListItem')) if matches == [] || \ (matches[1] ==? '' && matches[2] ==? '') || \ (matches[1] !=? '' && matches[2] !=? '') let item.type = 0 return item endif let item.cb = matches[3] if matches[1] !=? '' let item.type = 1 let item.mrkr = matches[1] else let item.type = 2 let item.mrkr = matches[2] endif return item endfunction function! s:empty_item() abort return {'type': 0} endfunction "Returns: level of the line "0 is the 'highest' level function! s:get_level(lnum) abort if getline(a:lnum) =~# '^\s*$' return 0 endif if !vimwiki#vars#get_syntaxlocal('recurring_bullets') let level = indent(a:lnum) else let level = s:string_length(matchstr(getline(a:lnum), \ vimwiki#vars#get_syntaxlocal('rx_bullet_chars')))-1 if level < 0 let level = (indent(a:lnum) == 0) ? 0 : 9999 endif endif return level endfunction "Returns: 1, a, i, A, I or '' "If in doubt if alphanumeric character or romanian "numeral, peek in the previous line function! s:guess_kind_of_numbered_item(item) abort if a:item.type != 2 | return '' | endif let number_chars = a:item.mrkr[:-2] let divisor = a:item.mrkr[-1:] let number_kinds = vimwiki#vars#get_syntaxlocal('number_kinds') if number_chars =~# '\d\+' return '1' endif if number_chars =~# '\l\+' if number_chars !~# '^[ivxlcdm]\+' || index(number_kinds, 'i') == -1 return 'a' else let item_above = s:get_prev_list_item(a:item, 0) if item_above.type != 0 if index(number_kinds, 'a') == -1 || \ (item_above.mrkr[-1:] !=# divisor && number_chars =~# 'i\+') || \ s:increment_i(item_above.mrkr[:-2]) ==# number_chars return 'i' else return 'a' endif else if number_chars =~# 'i\+' || index(number_kinds, 'a') == -1 return 'i' else return 'a' endif endif endif endif if number_chars =~# '\u\+' if number_chars !~# '^[IVXLCDM]\+' || index(number_kinds, 'I') == -1 return 'A' else let item_above = s:get_prev_list_item(a:item, 0) if item_above.type != 0 if index(number_kinds, 'A') == -1 || \ (item_above.mrkr[-1:] !=# divisor && number_chars =~# 'I\+') || \ s:increment_I(item_above.mrkr[:-2]) ==# number_chars return 'I' else return 'A' endif else if number_chars =~# 'I\+' || index(number_kinds, 'A') == -1 return 'I' else return 'A' endif endif endif endif endfunction function! s:regexp_of_marker(item) abort if a:item.type == 1 return vimwiki#u#escape(a:item.mrkr) elseif a:item.type == 2 let number_divisors = vimwiki#vars#get_syntaxlocal('number_divisors') for ki in ['d', 'u', 'l'] let match = matchstr(a:item.mrkr, '\'.ki.'\+['.number_divisors.']') if match !=? '' return '\'.ki.'\+'.vimwiki#u#escape(match[-1:]) endif endfor else return '' endif endfunction " Returns: Whether or not the checkbox of a list item is [X] or [-] function! s:is_closed(item) abort let state = a:item.cb return state ==# vimwiki#vars#get_syntaxlocal('listsyms_list')[-1] \ || state ==# vimwiki#vars#get_global('listsym_rejected') endfunction " --------------------------------------------------------- " functions for navigating between items " --------------------------------------------------------- "Returns: the list item after a:item or an empty item "If a:ignore_kind is 1, the markers can differ function! s:get_next_list_item(item, ignore_kind) abort let org_lvl = s:get_level(a:item.lnum) if !a:ignore_kind let org_regex = s:regexp_of_marker(a:item) endif let cur_ln = s:get_next_line(a:item.lnum) while cur_ln <= line('$') let cur_lvl = s:get_level(cur_ln) if cur_lvl <= org_lvl if a:ignore_kind return s:get_any_item_of_level(cur_ln, cur_lvl, org_lvl) else return s:get_item_of_level(cur_ln, cur_lvl, org_lvl, org_regex) endif endif let cur_ln = s:get_next_line(cur_ln) endwhile return s:empty_item() endfunction "Returns: the list item before a:item or an empty item "If a:ignore_kind is 1, the markers can differ function! s:get_prev_list_item(item, ignore_kind) abort let org_lvl = s:get_level(a:item.lnum) if !a:ignore_kind let org_regex = s:regexp_of_marker(a:item) endif let cur_ln = s:get_prev_line(a:item.lnum) while cur_ln >= 1 let cur_lvl = s:get_level(cur_ln) if cur_lvl <= org_lvl if a:ignore_kind return s:get_any_item_of_level(cur_ln, cur_lvl, org_lvl) else return s:get_item_of_level(cur_ln, cur_lvl, org_lvl, org_regex) endif endif let cur_ln = s:get_prev_line(cur_ln) endwhile return s:empty_item() endfunction function! s:get_item_of_level(cur_ln, cur_lvl, org_lvl, org_regex) abort let cur_linecontent = getline(a:cur_ln) if a:cur_lvl == a:org_lvl if cur_linecontent =~# '^\s*'.a:org_regex.'\s' return s:get_item(a:cur_ln) else return s:empty_item() endif elseif a:cur_lvl < a:org_lvl return s:empty_item() endif endfunction function! s:get_any_item_of_level(cur_ln, cur_lvl, org_lvl) abort if a:cur_lvl == a:org_lvl return s:get_item(a:cur_ln) elseif a:cur_lvl < a:org_lvl return s:empty_item() endif endfunction function! s:get_first_item_in_list(item, ignore_kind) abort let cur_item = a:item while 1 let prev_item = s:get_prev_list_item(cur_item, a:ignore_kind) if prev_item.type == 0 break else let cur_item = prev_item endif endwhile return cur_item endfunction function! s:get_last_item_in_list(item, ignore_kind) abort let cur_item = a:item while 1 let next_item = s:get_next_list_item(cur_item, a:ignore_kind) if next_item.type == 0 break else let cur_item = next_item endif endwhile return cur_item endfunction "Returns: lnum+1 in most cases, but skips blank lines and preformatted text, "0 in case of nonvalid line. "If there is no second argument, 0 is returned at a header, otherwise the "header is skipped function! s:get_next_line(lnum, ...) abort if getline(a:lnum) =~# vimwiki#vars#get_syntaxlocal('rxPreStart') let cur_ln = a:lnum + 1 while cur_ln <= line('$') && getline(cur_ln) !~# vimwiki#vars#get_syntaxlocal('rxPreEnd') let cur_ln += 1 endwhile let next_line = cur_ln + 1 else let next_line = a:lnum + 1 endif let next_line = nextnonblank(next_line) if a:0 > 0 && getline(next_line) =~# vimwiki#vars#get_syntaxlocal('rxHeader') let next_line = s:get_next_line(next_line, 1) endif if next_line < 0 || next_line > line('$') || \ (getline(next_line) =~# vimwiki#vars#get_syntaxlocal('rxHeader') && a:0 == 0) return 0 endif return next_line endfunction "Returns: lnum-1 in most cases, but skips blank lines and preformatted text "0 in case of nonvalid line and a header, because a header ends every list function! s:get_prev_line(lnum) abort let cur_ln = a:lnum - 1 if getline(cur_ln) =~# vimwiki#vars#get_syntaxlocal('rxPreEnd') while 1 if cur_ln == 0 || getline(cur_ln) =~# vimwiki#vars#get_syntaxlocal('rxPreStart') break endif let cur_ln -= 1 endwhile endif let prev_line = prevnonblank(cur_ln) if prev_line < 0 || prev_line > line('$') || \ getline(prev_line) =~# vimwiki#vars#get_syntaxlocal('rxHeader') return 0 endif return prev_line endfunction function! s:get_first_child(item) abort if a:item.lnum >= line('$') return s:empty_item() endif let org_lvl = s:get_level(a:item.lnum) let cur_item = s:get_item(s:get_next_line(a:item.lnum)) while 1 if cur_item.type != 0 && s:get_level(cur_item.lnum) > org_lvl return cur_item endif if cur_item.lnum > line('$') || cur_item.lnum <= 0 || s:get_level(cur_item.lnum) <= org_lvl return s:empty_item() endif let cur_item = s:get_item(s:get_next_line(cur_item.lnum)) endwhile endfunction "Returns: the next sibling of a:child, given the parent item "Used for iterating over children "Note: child items do not necessarily have the same indent, i.e. level function! s:get_next_child_item(parent, child) abort if a:parent.type == 0 | return s:empty_item() | endif let parent_lvl = s:get_level(a:parent.lnum) let cur_ln = s:get_last_line_of_item_incl_children(a:child) while 1 let next_line = s:get_next_line(cur_ln) if next_line == 0 || s:get_level(next_line) <= parent_lvl break endif let cur_ln = next_line let cur_item = s:get_item(cur_ln) if cur_item.type > 0 return cur_item endif endwhile return s:empty_item() endfunction function! s:get_parent(item) abort let parent_line = 0 let cur_ln = prevnonblank(a:item.lnum) let child_lvl = s:get_level(cur_ln) if child_lvl == 0 return s:empty_item() endif while 1 let cur_ln = s:get_prev_line(cur_ln) if cur_ln == 0 | break | endif let cur_lvl = s:get_level(cur_ln) if cur_lvl < child_lvl let cur_item = s:get_item(cur_ln) if cur_item.type == 0 let child_lvl = cur_lvl continue endif let parent_line = cur_ln break endif endwhile return s:get_item(parent_line) endfunction "Returns: the item above or the item below or an empty item function! s:get_a_neighbor_item(item) abort let prev_item = s:get_prev_list_item(a:item, 1) if prev_item.type != 0 return prev_item else let next_item = s:get_next_list_item(a:item, 1) if next_item.type != 0 return next_item endif endif return s:empty_item() endfunction function! s:get_a_neighbor_item_in_column(lnum, column) abort let cur_ln = s:get_prev_line(a:lnum) while cur_ln >= 1 if s:get_level(cur_ln) <= a:column return s:get_corresponding_item(cur_ln) endif let cur_ln = s:get_prev_line(cur_ln) endwhile return s:empty_item() endfunction "Returns: the item if there is one in a:lnum "else the multiline item a:lnum belongs to function! s:get_corresponding_item(lnum) abort let item = s:get_item(a:lnum) if item.type != 0 return item endif let org_lvl = s:get_level(a:lnum) let cur_ln = a:lnum while cur_ln > 0 let cur_lvl = s:get_level(cur_ln) let cur_item = s:get_item(cur_ln) if cur_lvl < org_lvl && cur_item.type != 0 return cur_item endif if cur_lvl < org_lvl let org_lvl = cur_lvl endif let cur_ln = s:get_prev_line(cur_ln) endwhile return s:empty_item() endfunction "Returns: the last line of a (possibly multiline) item, including all children function! s:get_last_line_of_item_incl_children(item) abort let cur_ln = a:item.lnum let org_lvl = s:get_level(a:item.lnum) while 1 let next_line = s:get_next_line(cur_ln) if next_line == 0 || s:get_level(next_line) <= org_lvl return cur_ln endif let cur_ln = next_line endwhile endfunction "Returns: the last line of a (possibly multiline) item "Note: there can be other list items between the first and last line function! s:get_last_line_of_item(item) abort if a:item.type == 0 | return 0 | endif let org_lvl = s:get_level(a:item.lnum) let last_corresponding_line = a:item.lnum let cur_ln = s:get_next_line(a:item.lnum) while 1 if cur_ln == 0 || s:get_level(cur_ln) <= org_lvl break endif let cur_item = s:get_item(cur_ln) if cur_item.type == 0 let last_corresponding_line = cur_ln let cur_ln = s:get_next_line(cur_ln) else let cur_ln = s:get_next_line(s:get_last_line_of_item_incl_children(cur_item)) endif endwhile return last_corresponding_line endfunction " --------------------------------------------------------- " renumber list items " --------------------------------------------------------- "Renumbers the current list from a:item on downwards "Returns: the last item that was adjusted function! s:adjust_numbered_list_below(item, recursive) abort if !(a:item.type == 2 || (a:item.type == 1 && a:recursive)) return a:item endif let kind = s:guess_kind_of_numbered_item(a:item) let cur_item = a:item while 1 if a:recursive call s:adjust_items_recursively(cur_item) endif let next_item = s:get_next_list_item(cur_item, 0) if next_item.type == 0 break endif if cur_item.type == 2 let new_val = s:increment_{kind}(cur_item.mrkr[:-2]) . cur_item.mrkr[-1:] call s:substitute_string_in_line(next_item.lnum, next_item.mrkr, new_val) let next_item.mrkr = new_val endif let cur_item = next_item endwhile return cur_item endfunction function! s:adjust_items_recursively(parent) abort if a:parent.type == 0 return s:empty_item() end let child_item = s:get_first_child(a:parent) if child_item.type == 0 return child_item endif while 1 let last_item = s:adjust_numbered_list(child_item, 1, 1) let child_item = s:get_next_child_item(a:parent, last_item) if child_item.type == 0 return last_item endif endwhile endfunction "Renumbers the list a:item is in. "If a:ignore_kind == 0, only the items which have the same kind of marker as "a:item are considered, otherwise all items. "Returns: the last item that was adjusted function! s:adjust_numbered_list(item, ignore_kind, recursive) abort if !(a:item.type == 2 || (a:item.type == 1 && (a:ignore_kind || a:recursive))) return s:empty_item() end let first_item = s:get_first_item_in_list(a:item, a:ignore_kind) while 1 if first_item.type == 2 let new_mrkr = s:guess_kind_of_numbered_item(first_item) . first_item.mrkr[-1:] call s:substitute_string_in_line(first_item.lnum, first_item.mrkr, new_mrkr) let first_item.mrkr = new_mrkr endif let last_item = s:adjust_numbered_list_below(first_item, a:recursive) let next_first_item = s:get_next_list_item(last_item, 1) if a:ignore_kind == 0 || next_first_item.type == 0 return last_item endif let first_item = next_first_item endwhile endfunction "Renumbers the list the cursor is in "also update its parents checkbox state function! vimwiki#lst#adjust_numbered_list() abort let cur_item = s:get_corresponding_item(line('.')) if cur_item.type == 0 | return | endif call s:adjust_numbered_list(cur_item, 1, 0) call s:update_state(s:get_parent(cur_item)) endfunction "Renumbers all lists of the buffer "of course, this might take some seconds function! vimwiki#lst#adjust_whole_buffer() abort let cur_ln = 1 while 1 let cur_item = s:get_item(cur_ln) if cur_item.type != 0 let cur_item = s:adjust_numbered_list(cur_item, 0, 1) endif let cur_ln = s:get_next_line(cur_item.lnum, 1) if cur_ln <= 0 || cur_ln > line('$') return endif endwhile endfunction " --------------------------------------------------------- " checkbox stuff " --------------------------------------------------------- "Returns: the rate of checkboxed list item in percent function! s:get_rate(item) abort if a:item.type == 0 || a:item.cb ==? '' return -1 endif let state = a:item.cb if state == vimwiki#vars#get_global('listsym_rejected') return -1 endif let n = len(vimwiki#vars#get_syntaxlocal('listsyms_list')) return index(vimwiki#vars#get_syntaxlocal('listsyms_list'), state) * 100/(n-1) endfunction "Set state of the list item to [ ] or [o] or whatever "Returns: 1 if the state changed, 0 otherwise function! s:set_state(item, new_rate) abort let new_state = s:rate_to_state(a:new_rate) let old_state = s:rate_to_state(s:get_rate(a:item)) if new_state !=# old_state call s:substitute_rx_in_line(a:item.lnum, '\[.]', '['.new_state.']') return 1 else return 0 endif endfunction " Sets the state of the list item to [ ] or [o] or whatever. Updates the states of its child items. " If the new state should be [X] or [-], the state of the current list item is changed to this " state, but if a child item already has [X] or [-] it is left alone. function! s:set_state_plus_children(item, new_rate, ...) abort let retain_state_if_closed = a:0 > 0 && a:1 > 0 if !(retain_state_if_closed && (a:new_rate == 100 || a:new_rate == -1) && s:is_closed(a:item)) call s:set_state(a:item, a:new_rate) endif let all_children_are_done = 1 let all_children_are_rejected = 1 let child_item = s:get_first_child(a:item) while 1 if child_item.type == 0 break endif if child_item.cb != vimwiki#vars#get_global('listsym_rejected') let all_children_are_rejected = 0 endif if child_item.cb != vimwiki#vars#get_syntaxlocal('listsyms_list')[-1] let all_children_are_done = 0 endif if !all_children_are_done && !all_children_are_rejected break endif let child_item = s:get_next_child_item(a:item, child_item) endwhile if (a:new_rate == 100 && all_children_are_done) || \ (a:new_rate == -1) && all_children_are_rejected return endif if (a:new_rate == -1 && all_children_are_done) || \ (a:new_rate == 100 && all_children_are_rejected) let retain_closed_children = 0 else let retain_closed_children = 1 endif let child_item = s:get_first_child(a:item) while 1 if child_item.type == 0 break endif if child_item.cb !=? '' call s:set_state_plus_children(child_item, a:new_rate, retain_closed_children) endif let child_item = s:get_next_child_item(a:item, child_item) endwhile endfunction "Returns: the appropriate symbol for a given percent rate function! s:rate_to_state(rate) abort let listsyms_list = vimwiki#vars#get_syntaxlocal('listsyms_list') let state = '' let n = len(listsyms_list) if a:rate == 100 let state = listsyms_list[n-1] elseif a:rate == 0 let state = listsyms_list[0] elseif a:rate == -1 let state = vimwiki#vars#get_global('listsym_rejected') else let index = float2nr(ceil(a:rate/100.0*(n-2))) let state = listsyms_list[index] endif return state endfunction "updates the symbol of a checkboxed item according to the symbols of its "children function! s:update_state(item) abort if a:item.type == 0 || a:item.cb ==? '' return endif let sum_children_rate = 0 let count_children_with_cb = 0 let count_rejected_children = 0 let child_item = s:get_first_child(a:item) while 1 if child_item.type == 0 break endif if child_item.cb !=? '' let rate = s:get_rate(child_item) if rate == -1 " for calculating the parent rate, a [-] item counts as much as a [X] item ... let rate = 100 " ... with the exception that a parent with *only* [-] items will be [-] too let count_rejected_children += 1 endif let count_children_with_cb += 1 let sum_children_rate += rate endif let child_item = s:get_next_child_item(a:item, child_item) endwhile if count_children_with_cb > 0 if count_rejected_children == count_children_with_cb let new_rate = -1 else let new_rate = sum_children_rate / count_children_with_cb endif call s:set_state_recursively(a:item, new_rate) else let rate = s:get_rate(a:item) if rate > 0 && rate < 100 call s:set_state_recursively(a:item, 0) endif endif endfunction function! s:set_state_recursively(item, new_rate) abort let state_changed = s:set_state(a:item, a:new_rate) if state_changed call s:update_state(s:get_parent(a:item)) endif endfunction "Creates checkbox in a list item. "Returns: 1 if successful function! s:create_cb(item, start_rate) abort if a:item.type == 0 || a:item.cb !=? '' return 0 endif let new_item = a:item let new_item.cb = s:rate_to_state(a:start_rate) call s:substitute_rx_in_line(new_item.lnum, \ vimwiki#u#escape(new_item.mrkr) . '\zs\ze', ' [' . new_item.cb . ']') call s:update_state(new_item) return 1 endfunction function! s:remove_cb(item) abort let item = a:item if item.type != 0 && item.cb !=? '' let item.cb = '' call s:substitute_rx_in_line(item.lnum, '\s\+\[.\]', '') endif return item endfunction " Change state of the checkboxes in the lines of the given range function! s:change_cb(from_line, to_line, new_rate) abort let from_item = s:get_corresponding_item(a:from_line) if from_item.type == 0 return endif let parent_items_of_lines = [] for cur_ln in range(from_item.lnum, a:to_line) let cur_item = s:get_item(cur_ln) if cur_item.type != 0 && cur_item.cb !=? '' call s:set_state_plus_children(cur_item, a:new_rate) let cur_parent_item = s:get_parent(cur_item) if index(parent_items_of_lines, cur_parent_item) == -1 call insert(parent_items_of_lines, cur_parent_item) endif endif endfor for parent_item in parent_items_of_lines call s:update_state(parent_item) endfor endfunction " Toggles checkbox between two states in the lines of the given range, creates checkboxes (with " a:start_rate as state) if there aren't any. function! s:toggle_create_cb(from_line, to_line, state1, state2, start_rate) abort let from_item = s:get_corresponding_item(a:from_line) if from_item.type == 0 return endif if from_item.cb ==? '' "if from_line has no CB, make a CB in every selected line let parent_items_of_lines = [] for cur_ln in range(from_item.lnum, a:to_line) let cur_item = s:get_item(cur_ln) let success = s:create_cb(cur_item, a:start_rate) if success let cur_parent_item = s:get_parent(cur_item) if index(parent_items_of_lines, cur_parent_item) == -1 call insert(parent_items_of_lines, cur_parent_item) endif endif endfor for parent_item in parent_items_of_lines call s:update_state(parent_item) endfor else "if from_line has CB, toggle it and set all siblings to the same new state let rate_first_line = s:get_rate(from_item) let new_rate = rate_first_line == a:state1 ? a:state2 : a:state1 call s:change_cb(a:from_line, a:to_line, new_rate) endif endfunction "Decrement checkbox between [ ] and [X] "in the lines of the given range function! vimwiki#lst#decrement_cb(from_line, to_line) abort let from_item = s:get_corresponding_item(a:from_line) if from_item.type == 0 return endif "if from_line has CB, decrement it and set all siblings to the same new state let rate_first_line = s:get_rate(from_item) let n = len(vimwiki#vars#get_syntaxlocal('listsyms_list')) let new_rate = max([rate_first_line - 100/(n-1)-1, 0]) call s:change_cb(a:from_line, a:to_line, new_rate) endfunction "Increment checkbox between [ ] and [X] "in the lines of the given range function! vimwiki#lst#increment_cb(from_line, to_line) abort let from_item = s:get_corresponding_item(a:from_line) if from_item.type == 0 return endif "if from_line has CB, increment it and set all siblings to the same new state let rate_first_line = s:get_rate(from_item) let n = len(vimwiki#vars#get_syntaxlocal('listsyms_list')) let new_rate = min([rate_first_line + 100/(n-1)+1, 100]) call s:change_cb(a:from_line, a:to_line, new_rate) endfunction "Toggles checkbox between [ ] and [X] or creates one "in the lines of the given range function! vimwiki#lst#toggle_cb(from_line, to_line) abort return s:toggle_create_cb(a:from_line, a:to_line, 100, 0, 0) endfunction "Toggles checkbox between [ ] and [-] or creates one "in the lines of the given range function! vimwiki#lst#toggle_rejected_cb(from_line, to_line) abort return s:toggle_create_cb(a:from_line, a:to_line, -1, 0, -1) endfunction function! vimwiki#lst#remove_cb(first_line, last_line) abort let first_item = s:get_corresponding_item(a:first_line) let last_item = s:get_corresponding_item(a:last_line) if first_item.type == 0 || last_item.type == 0 return endif let parent_items_of_lines = [] let cur_ln = first_item.lnum while 1 if cur_ln <= 0 || cur_ln > last_item.lnum | break | endif let cur_item = s:get_item(cur_ln) if cur_item.type != 0 let cur_item = s:remove_cb(cur_item) let cur_parent_item = s:get_parent(cur_item) if index(parent_items_of_lines, cur_parent_item) == -1 call insert(parent_items_of_lines, cur_parent_item) endif endif let cur_ln = s:get_next_line(cur_ln) endwhile for parent_item in parent_items_of_lines call s:update_state(parent_item) endfor endfunction function! vimwiki#lst#remove_cb_in_list() abort let first_item = s:get_first_item_in_list(s:get_corresponding_item(line('.')), 0) let cur_item = first_item while 1 let next_item = s:get_next_list_item(cur_item, 0) let cur_item = s:remove_cb(cur_item) if next_item.type == 0 break else let cur_item = next_item endif endwhile call s:update_state(s:get_parent(first_item)) endfunction " --------------------------------------------------------- " change the level of list items " --------------------------------------------------------- function! s:set_indent(lnum, new_indent) abort if &expandtab let indentstring = repeat(' ', a:new_indent) else let indentstring = repeat('\t', a:new_indent / &tabstop) . repeat(' ', a:new_indent % &tabstop) endif call s:substitute_rx_in_line(a:lnum, '^\s*', indentstring) endfunction function! s:decrease_level(item) abort let removed_indent = 0 if vimwiki#vars#get_syntaxlocal('recurring_bullets') && a:item.type == 1 && \ index(vimwiki#vars#get_syntaxlocal('multiple_bullet_chars'), \ s:first_char(a:item.mrkr)) > -1 if s:string_length(a:item.mrkr) >= 2 call s:substitute_string_in_line(a:item.lnum, s:first_char(a:item.mrkr), '') let removed_indent = -1 endif else let old_indent = indent(a:item.lnum) if &shiftround let new_indent = (old_indent - 1) / vimwiki#u#sw() * vimwiki#u#sw() else let new_indent = old_indent - vimwiki#u#sw() endif call s:set_indent(a:item.lnum, new_indent) let removed_indent = new_indent - old_indent endif return removed_indent endfunction function! s:increase_level(item) abort let additional_indent = 0 if vimwiki#vars#get_syntaxlocal('recurring_bullets') && a:item.type == 1 && \ index(vimwiki#vars#get_syntaxlocal('multiple_bullet_chars'), \ s:first_char(a:item.mrkr)) > -1 call s:substitute_string_in_line(a:item.lnum, a:item.mrkr, a:item.mrkr . \ s:first_char(a:item.mrkr)) let additional_indent = 1 else let old_indent = indent(a:item.lnum) if &shiftround let new_indent = (old_indent / vimwiki#u#sw() + 1) * vimwiki#u#sw() else let new_indent = old_indent + vimwiki#u#sw() endif call s:set_indent(a:item.lnum, new_indent) let additional_indent = new_indent - old_indent endif return additional_indent endfunction "adds a:indent_by to the current indent "a:indent_by can be negative function! s:indent_line_by(lnum, indent_by) abort let item = s:get_item(a:lnum) if vimwiki#vars#get_syntaxlocal('recurring_bullets') && item.type == 1 && \ index(vimwiki#vars#get_syntaxlocal('multiple_bullet_chars'), \ s:first_char(item.mrkr)) > -1 if a:indent_by > 0 call s:substitute_string_in_line(a:lnum, item.mrkr, item.mrkr . s:first_char(item.mrkr)) elseif a:indent_by < 0 call s:substitute_string_in_line(a:lnum, s:first_char(item.mrkr), '') endif else call s:set_indent(a:lnum, indent(a:lnum) + a:indent_by) endif endfunction "changes lvl of lines in selection function! s:change_level(from_line, to_line, direction, plus_children) abort let from_item = s:get_corresponding_item(a:from_line) if from_item.type == 0 if a:direction ==# 'increase' && a:from_line == a:to_line && empty(getline(a:from_line)) "that's because :> doesn't work on an empty line exe 'normal!' "gi\" else execute a:from_line.','.a:to_line.(a:direction ==# 'increase' ? '>' : '<') endif return endif if a:direction ==# 'decrease' && s:get_level(from_item.lnum) == 0 return endif if a:from_line == a:to_line if a:plus_children let to_line = s:get_last_line_of_item_incl_children(from_item) else let to_line = s:get_last_line_of_item(from_item) endif else let to_item = s:get_corresponding_item(a:to_line) if to_item.type == 0 let to_line = a:to_line else if a:plus_children let to_line = s:get_last_line_of_item_incl_children(to_item) else let to_line = s:get_last_line_of_item(to_item) endif endif endif if to_line == 0 return endif let to_be_adjusted = s:get_a_neighbor_item(from_item) let old_parent = s:get_parent(from_item) let first_line_level = s:get_level(from_item.lnum) let more_than_one_level_concerned = 0 let first_line_indented_by = (a:direction ==# 'increase') ? \ s:increase_level(from_item) : s:decrease_level(from_item) let cur_ln = s:get_next_line(from_item.lnum) while cur_ln > 0 && cur_ln <= to_line if !more_than_one_level_concerned && \ s:get_level(cur_ln) != first_line_level && \ s:get_item(cur_ln).type != 0 let more_than_one_level_concerned = 1 endif call s:indent_line_by(cur_ln, first_line_indented_by) let cur_ln = s:get_next_line(cur_ln, 1) endwhile if a:from_line == a:to_line call s:adjust_mrkr(from_item) endif call s:update_state(old_parent) let from_item = s:get_item(from_item.lnum) if from_item.cb !=? '' call s:update_state(from_item) call s:update_state(s:get_parent(from_item)) endif if more_than_one_level_concerned call vimwiki#lst#adjust_whole_buffer() else call s:adjust_numbered_list(from_item, 0, 0) call s:adjust_numbered_list(to_be_adjusted, 0, 0) endif endfunction function! vimwiki#lst#change_level(from_line, to_line, direction, plus_children) abort let cur_col = col('$') - col('.') call s:change_level(a:from_line, a:to_line, a:direction, a:plus_children) call cursor('.', col('$') - cur_col) endfunction "indent line a:lnum to be the continuation of a:prev_item function! s:indent_multiline(prev_item, lnum) abort if a:prev_item.type != 0 call s:set_indent(a:lnum, s:text_begin(a:prev_item.lnum)) endif endfunction " --------------------------------------------------------- " change markers of list items " --------------------------------------------------------- "Returns: the position of a marker in g:vimwiki_list_markers function! s:get_idx_list_markers(item) abort if a:item.type == 1 let m = s:first_char(a:item.mrkr) else let m = s:guess_kind_of_numbered_item(a:item) . a:item.mrkr[-1:] endif return index(vimwiki#vars#get_syntaxlocal('list_markers'), m) endfunction "changes the marker of the given item to the next in g:vimwiki_list_markers function! s:get_next_mrkr(item) abort let markers = vimwiki#vars#get_syntaxlocal('list_markers') if a:item.type == 0 let new_mrkr = markers[0] else let idx = s:get_idx_list_markers(a:item) let new_mrkr = markers[(idx+1) % len(markers)] endif return new_mrkr endfunction "changes the marker of the given item to the previous in g:vimwiki_list_markers function! s:get_prev_mrkr(item) abort let markers = vimwiki#vars#get_syntaxlocal('list_markers') if a:item.type == 0 return markers[-1] endif let idx = s:get_idx_list_markers(a:item) if idx == -1 return markers[-1] else return markers[(idx - 1 + len(markers)) % len(markers)] endif endfunction function! s:set_new_mrkr(item, new_mrkr) abort if a:item.type == 0 call s:substitute_rx_in_line(a:item.lnum, '^\s*\zs\ze', a:new_mrkr.' ') if indent(a:item.lnum) == 0 && !vimwiki#vars#get_syntaxlocal('recurring_bullets') call s:set_indent(a:item.lnum, vimwiki#lst#get_list_margin()) endif else call s:substitute_string_in_line(a:item.lnum, a:item.mrkr, a:new_mrkr) endif endfunction function! vimwiki#lst#change_marker(from_line, to_line, new_mrkr, mode) abort let cur_col_from_eol = col('$') - (a:mode ==# 'i' ? col("'^") : col('.')) let new_mrkr = a:new_mrkr let cur_ln = a:from_line while 1 let cur_item = s:get_item(cur_ln) if new_mrkr ==# 'next' let new_mrkr = s:get_next_mrkr(cur_item) elseif new_mrkr ==# 'prev' let new_mrkr = s:get_prev_mrkr(cur_item) endif "handle markers like *** if index(vimwiki#vars#get_syntaxlocal('multiple_bullet_chars'), s:first_char(new_mrkr)) > -1 "use *** if the item above has *** too let item_above = s:get_prev_list_item(cur_item, 1) if item_above.type == 1 && s:first_char(item_above.mrkr) ==# s:first_char(new_mrkr) let new_mrkr = item_above.mrkr else "use *** if the item below has *** too let item_below = s:get_next_list_item(cur_item, 1) if item_below.type == 1 && s:first_char(item_below.mrkr) ==# s:first_char(new_mrkr) let new_mrkr = item_below.mrkr else "if the old is ### and the new is * use *** if cur_item.type == 1 && \ index(vimwiki#vars#get_syntaxlocal('multiple_bullet_chars'), \ s:first_char(cur_item.mrkr))>-1 let new_mrkr = repeat(new_mrkr, s:string_length(cur_item.mrkr)) else "use *** if the parent item has ** let parent_item = s:get_parent(cur_item) if parent_item.type == 1 && s:first_char(parent_item.mrkr) ==# s:first_char(new_mrkr) let new_mrkr = repeat(s:first_char(parent_item.mrkr), \ s:string_length(parent_item.mrkr)+1) endif endif endif endif endif call s:set_new_mrkr(cur_item, new_mrkr) call s:adjust_numbered_list(s:get_item(cur_ln), 1, 0) if cur_ln >= a:to_line | break | endif let cur_ln = s:get_next_line(cur_ln, 1) endwhile call cursor('.', col('$') - cur_col_from_eol) endfunction function! vimwiki#lst#change_marker_in_list(new_mrkr) abort let cur_item = s:get_corresponding_item(line('.')) let first_item = s:get_first_item_in_list(cur_item, 0) let last_item = s:get_last_item_in_list(cur_item, 0) if first_item.type == 0 || last_item.type == 0 | return | endif let first_item_line = first_item.lnum let cur_item = first_item while cur_item.type != 0 && cur_item.lnum <= last_item.lnum call s:set_new_mrkr(cur_item, a:new_mrkr) let cur_item = s:get_next_list_item(cur_item, 1) endwhile call s:adjust_numbered_list(s:get_item(first_item_line), 0, 0) endfunction "sets kind of the item depending on neighbor items and the parent item function! s:adjust_mrkr(item) abort if a:item.type == 0 || vimwiki#vars#get_syntaxlocal('recurring_bullets') return endif let new_mrkr = a:item.mrkr let neighbor_item = s:get_a_neighbor_item(a:item) if neighbor_item.type != 0 let new_mrkr = neighbor_item.mrkr endif "if possible, set e.g. *** if parent has ** as marker if neighbor_item.type == 0 && a:item.type == 1 && \ index(vimwiki#vars#get_syntaxlocal('multiple_bullet_chars'), \ s:first_char(a:item.mrkr)) > -1 let parent_item = s:get_parent(a:item) if parent_item.type == 1 && s:first_char(parent_item.mrkr) ==# s:first_char(a:item.mrkr) let new_mrkr = repeat(s:first_char(parent_item.mrkr), s:string_length(parent_item.mrkr)+1) endif endif call s:substitute_string_in_line(a:item.lnum, a:item.mrkr, new_mrkr) call s:adjust_numbered_list(a:item, 0, 1) endfunction function! s:clone_marker_from_to(from, to) abort let item_from = s:get_item(a:from) if item_from.type == 0 | return | endif let new_mrkr = item_from.mrkr . ' ' call s:substitute_rx_in_line(a:to, '^\s*', new_mrkr) let new_indent = ( vimwiki#vars#get_syntaxlocal('recurring_bullets') ? 0 : indent(a:from) ) call s:set_indent(a:to, new_indent) if item_from.cb !=? '' call s:create_cb(s:get_item(a:to), 0) call s:update_state(s:get_parent(s:get_item(a:to))) endif if item_from.type == 2 let adjust_from = ( a:from < a:to ? a:from : a:to ) call s:adjust_numbered_list_below(s:get_item(adjust_from), 0) endif endfunction function! s:remove_mrkr(item) abort let item = a:item if item.cb !=? '' let item = s:remove_cb(item) let parent_item = s:get_parent(item) else let parent_item = s:empty_item() endif call s:substitute_rx_in_line(item.lnum, vimwiki#u#escape(item.mrkr).'\s*', '') call remove(item, 'mrkr') call remove(item, 'cb') let item.type = 0 call s:update_state(parent_item) return item endfunction function! s:create_marker(lnum) abort let new_sibling = s:get_corresponding_item(a:lnum) if new_sibling.type == 0 let new_sibling = s:get_a_neighbor_item_in_column(a:lnum, virtcol('.')) endif if new_sibling.type != 0 call s:clone_marker_from_to(new_sibling.lnum, a:lnum) else let cur_item = s:get_item(a:lnum) call s:set_new_mrkr(cur_item, vimwiki#vars#get_syntaxlocal('list_markers')[0]) call s:adjust_numbered_list(cur_item, 0, 0) endif endfunction " --------------------------------------------------------- " handle keys " --------------------------------------------------------- function! vimwiki#lst#kbd_o() abort let fold_end = foldclosedend('.') let lnum = (fold_end == -1) ? line('.') : fold_end let cur_item = s:get_item(lnum) let parent = s:get_corresponding_item(lnum) "inserting and deleting the x is necessary "because otherwise the indent is lost exe 'normal!' "ox\" if !vimwiki#u#is_codeblock(lnum) if parent.type != 0 call s:clone_marker_from_to(parent.lnum, cur_item.lnum+1) else call s:indent_multiline(cur_item, cur_item.lnum+1) endif endif startinsert! endfunction function! vimwiki#lst#kbd_O() abort exe 'normal!' "Ox\" let cur_ln = line('.') if !vimwiki#u#is_codeblock(cur_ln) if getline(cur_ln+1) !~# '^\s*$' call s:clone_marker_from_to(cur_ln+1, cur_ln) else call s:clone_marker_from_to(cur_ln-1, cur_ln) endif endif startinsert! endfunction function! s:cr_on_empty_list_item(lnum, behavior) abort if a:behavior == 1 "just make a new list item exe 'normal!' "gi\\" call s:clone_marker_from_to(a:lnum, a:lnum+1) startinsert! return elseif a:behavior == 2 "insert new marker but remove marker in old line call append(a:lnum-1, '') startinsert! return elseif a:behavior == 3 "list is finished, but cursor stays in current line let item = s:get_item(a:lnum) let neighbor_item = s:get_a_neighbor_item(item) let child_item = s:get_first_child(item) let parent_item = (item.cb !=? '') ? s:get_parent(item) : s:empty_item() normal! "_cc call s:adjust_numbered_list(neighbor_item, 0, 0) call s:adjust_numbered_list(child_item, 0, 0) call s:update_state(parent_item) startinsert return elseif a:behavior == 4 "list is finished, but cursor goes to next line let item = s:get_item(a:lnum) let neighbor_item = s:get_a_neighbor_item(item) let child_item = s:get_first_child(item) let parent_item = (item.cb !=? '') ? s:get_parent(item) : s:empty_item() exe 'normal!' "_cc\" call s:adjust_numbered_list(neighbor_item, 0, 0) call s:adjust_numbered_list(child_item, 0, 0) call s:update_state(parent_item) startinsert return elseif a:behavior == 5 "successively decrease level if s:get_level(a:lnum) > 0 call s:change_level(a:lnum, a:lnum, 'decrease', 0) startinsert! else let item = s:get_item(a:lnum) let neighbor_item = s:get_a_neighbor_item(item) let child_item = s:get_first_child(item) let parent_item = (item.cb !=? '') ? s:get_parent(item) : s:empty_item() normal! "_cc call s:adjust_numbered_list(neighbor_item, 0, 0) call s:adjust_numbered_list(child_item, 0, 0) call s:update_state(parent_item) startinsert endif return endif endfunction function! s:cr_on_empty_line(lnum, behavior) abort let lst = s:get_corresponding_item(a:lnum) "inserting and deleting the x is necessary "because otherwise the indent is lost exe 'normal!' "gi\x\\" if a:behavior == 2 || a:behavior == 3 if lst.type == 0 || vimwiki#u#is_codeblock(a:lnum) " don't insert new bullet if not part of a list return else call s:create_marker(a:lnum+1) endif endif endfunction function! s:cr_on_list_item(lnum, insert_new_marker, not_at_eol) abort if a:insert_new_marker "the ultimate feature of this script: make new marker on exe 'normal!' "gi\\" call s:clone_marker_from_to(a:lnum, a:lnum+1) "tiny sweet extra feature: indent next line if current line ends with : if !a:not_at_eol && getline(a:lnum) =~# ':$' call s:change_level(a:lnum+1, a:lnum+1, 'increase', 0) endif else " || (cur_item.lnum < s:get_last_line_of_item(cur_item)) "indent this line so that it becomes the continuation of the line above exe 'normal!' "gi\\" let prev_line = s:get_corresponding_item(s:get_prev_line(a:lnum+1)) call s:indent_multiline(prev_line, a:lnum+1) endif endfunction function! vimwiki#lst#kbd_cr(normal, just_mrkr) abort let lnum = line('.') let has_bp = s:line_has_marker(lnum) if has_bp != 0 && virtcol('.') < s:text_begin(lnum) call append(lnum-1, '') startinsert! return endif if has_bp == 1 call s:cr_on_empty_list_item(lnum, a:just_mrkr) return endif let insert_new_marker = (a:normal == 1 || a:normal == 3) if getline('.')[col("'^")-1:] =~# '^\s\+$' let cur_col = 0 else let cur_col = col('$') - col("'^") if getline('.')[col("'^")-1] =~# '\s' && exists('*strdisplaywidth') let ws_behind_cursor = \ strdisplaywidth(matchstr(getline('.')[col("'^")-1:], '\s\+'), \ virtcol("'^")-1) let cur_col -= ws_behind_cursor endif if insert_new_marker && cur_col == 0 && getline(lnum) =~# '\s$' let insert_new_marker = 0 endif endif if has_bp == 0 call s:cr_on_empty_line(lnum, a:normal) endif if has_bp == 2 call s:cr_on_list_item(lnum, insert_new_marker, cur_col) endif call cursor(lnum+1, col('$') - cur_col) if cur_col == 0 startinsert! else startinsert endif endfunction "creates a list item in the current line or removes it function! vimwiki#lst#toggle_list_item() abort let cur_col_from_eol = col('$') - col("'^") let cur_item = s:get_item(line('.')) if cur_item.type == 0 call s:create_marker(cur_item.lnum) else let prev_item = s:get_prev_list_item(cur_item, 1) if prev_item.type == 0 let prev_item = s:get_corresponding_item(s:get_prev_line(cur_item.lnum)) endif let cur_item = s:remove_mrkr(cur_item) let adjust_prev_item = (prev_item.type == 2 && \ s:get_level(cur_item.lnum) <= s:get_level(prev_item.lnum)) ? 1 : 0 call s:indent_multiline(prev_item, cur_item.lnum) if adjust_prev_item call s:adjust_numbered_list_below(prev_item, 0) endif endif "set cursor position s.t. it's on the same char as before let new_cur_col = col('$') - cur_col_from_eol call cursor(cur_item.lnum, new_cur_col >= 1 ? new_cur_col : 1) if cur_col_from_eol == 0 || getline(cur_item.lnum) =~# '^\s*$' startinsert! else startinsert endif endfunction " --------------------------------------------------------- " misc stuff " --------------------------------------------------------- function! vimwiki#lst#TO_list_item(inner, visual) abort let lnum = prevnonblank('.') let item = s:get_corresponding_item(lnum) if item.type == 0 return endif let from_line = item.lnum if a:inner let to_line = s:get_last_line_of_item(item) else let to_line = s:get_last_line_of_item_incl_children(item) endif normal! V call cursor(to_line, 0) normal! o call cursor(from_line, 0) endfunction function! vimwiki#lst#fold_level(lnum) abort let cur_item = s:get_item(a:lnum) if cur_item.type != 0 let parent_item = s:get_parent(cur_item) let child_item = s:get_first_child(cur_item) let next_item = s:get_next_child_item(parent_item, cur_item) if child_item.type != 0 return 'a1' elseif next_item.type == 0 let c_indent = indent(a:lnum) / &shiftwidth let n_indent = indent(a:lnum+1) / &shiftwidth return 's' . (c_indent - n_indent) endif endif return '=' endfunction