11" Vim plugin for automated bulleted lists
2- " Last Change: February 23 , 2020
2+ " Last Change: March 2 , 2020
33" Maintainer: Dorian Karter
44" License: MIT
55" FileTypes: markdown, text, gitcommit
@@ -55,6 +55,13 @@ while s:power >= 0
5555 let s: abc_max += pow (26 ,s: power )
5656 let s: power -= 1
5757endwhile
58+
59+ if ! exists (' g:bullets_outline_levels' )
60+ " Capitalization matters: all caps will make the symbol caps, lower = lower
61+ " Standard bullets should include the marker symbol after 'std'
62+ let g: bullets_outline_levels = [' ROM' , ' ABC' , ' num' , ' abc' , ' rom' , ' std-' , ' std*' , ' std+' ]
63+ endif
64+
5865" ------------------------------------------------------ }}}
5966
6067" Parse Bullet Type ------------------------------------------- {{{
@@ -197,40 +204,50 @@ fun! s:match_checkbox_bullet_item(input_text)
197204endfun
198205
199206fun ! s: match_bullet_list_item (input_text)
200- let l: std_bullet_regex = ' \v(^\s*(-|\*+|\.+|#\.|\\item)(\s+))(.*)'
207+ let l: std_bullet_regex = ' \v(^( \s*) (-|\*+|\.+|#\.|\+ |\\item)(\s+))(.*)'
201208 let l: matches = matchlist (a: input_text , l: std_bullet_regex )
202209
203210 if empty (l: matches )
204211 return {}
205212 endif
206213
207214 let l: bullet_length = strlen (l: matches [1 ])
208- let l: whole_bullet = l: matches [1 ]
209- let l: trailing_space = l: matches [3 ]
210- let l: text_after_bullet = l: matches [4 ]
215+ let l: leading_space = l: matches [2 ]
216+ let l: bullet = l: matches [3 ]
217+ let l: trailing_space = l: matches [4 ]
218+ let l: text_after_bullet = l: matches [5 ]
211219
212220 return {
213221 \ ' bullet_type' : ' std' ,
214222 \ ' bullet_length' : l: bullet_length ,
215- \ ' whole_bullet' : l: whole_bullet ,
223+ \ ' leading_space' : l: leading_space ,
224+ \ ' bullet' : l: bullet ,
225+ \ ' closure' : ' ' ,
216226 \ ' trailing_space' : l: trailing_space ,
217227 \ ' text_after_bullet' : l: text_after_bullet
218228 \ }
219229endfun
220230" ------------------------------------------------------- }}}
221231
222232" Resolve Bullet Type ----------------------------------- {{{
223- fun ! s: closest_bullet_types (from_line_num)
233+ fun ! s: closest_bullet_types (from_line_num, max_indent )
224234 let l: lnum = a: from_line_num
225235 let l: ltxt = getline (l: lnum )
236+ let l: curr_indent = indent (l: lnum )
226237 let l: bullet_kinds = s: parse_bullet (l: lnum , l: ltxt )
227238
228239 " Support for wrapped text bullets
229240 " DEMO: https://raw.githubusercontent.com/dkarter/bullets.vim/master/img/wrapped-bullets.gif
230- while l: lnum > 1 && s: is_indented (l: ltxt ) && l: bullet_kinds == []
231- let l: lnum = l: lnum - 1
241+ while l: lnum > 1 && (l: curr_indent != 0 || l: bullet_kinds != [])
242+ \ && (a: max_indent < l: curr_indent || l: bullet_kinds == [])
243+ if l: bullet_kinds != []
244+ let l: lnum = l: lnum - g: bullets_line_spacing
245+ else
246+ let l: lnum = l: lnum - 1
247+ endif
232248 let l: ltxt = getline (l: lnum )
233249 let l: bullet_kinds = s: parse_bullet (l: lnum , l: ltxt )
250+ let l: curr_indent = indent (l: lnum )
234251 endwhile
235252
236253 return l: bullet_kinds
@@ -259,8 +276,14 @@ endfun
259276" Roman Numeral vs Alphabetic Bullets ---------------------------------- {{{
260277fun ! s: resolve_rom_or_abc (bullet_types)
261278 let l: first_type = a: bullet_types [0 ]
262- let l: prev_search_starting_line = get (l: first_type , ' starting_at_line_num' ) - 1
263- let l: prev_bullet_types = s: closest_bullet_types (l: prev_search_starting_line )
279+ let l: prev_search_starting_line = l: first_type .starting_at_line_num - g: bullets_line_spacing
280+ let l: bullet_indent = indent (l: first_type .starting_at_line_num)
281+ let l: prev_bullet_types = s: closest_bullet_types (l: prev_search_starting_line , l: bullet_indent )
282+
283+ while l: prev_bullet_types != [] && l: bullet_indent > indent (l: prev_search_starting_line )
284+ let l: prev_search_starting_line -= g: bullets_line_spacing
285+ let l: prev_bullet_types = s: closest_bullet_types (l: prev_search_starting_line , l: bullet_indent )
286+ endwhile
264287
265288 if len (l: prev_bullet_types ) == 0
266289
@@ -319,37 +342,36 @@ fun! s:next_bullet_str(bullet)
319342 let l: bullet_type = get (a: bullet , ' bullet_type' )
320343
321344 if l: bullet_type == # ' rom'
322- return s: next_rom_bullet (a: bullet )
345+ let l: next_bullet_marker = s: next_rom_bullet (a: bullet )
323346 elseif l: bullet_type == # ' abc'
324- return s: next_abc_bullet (a: bullet )
347+ let l: next_bullet_marker = s: next_abc_bullet (a: bullet )
325348 elseif l: bullet_type == # ' num'
326- return s: next_num_bullet (a: bullet )
349+ let l: next_bullet_marker = s: next_num_bullet (a: bullet )
327350 elseif l: bullet_type == # ' chk'
328- return s: next_chk_bullet (a: bullet )
351+ let l: next_bullet_marker = s: next_chk_bullet (a: bullet )
329352 else
330- return a: bullet .whole_bullet
353+ let l: next_bullet_marker = a: bullet .bullet
331354 endif
355+ let l: closure = has_key (a: bullet , ' closure' ) ? a: bullet .closure : ' '
356+ return a: bullet .leading_space . l: next_bullet_marker . l: closure . ' '
332357endfun
333358
334359fun ! s: next_rom_bullet (bullet)
335360 let l: islower = a: bullet .bullet == # tolower (a: bullet .bullet)
336- let l: next_num = s: arabic2roman (s: roman2arabic (a: bullet .bullet) + 1 , l: islower )
337- return a: bullet .leading_space . l: next_num . a: bullet .closure . ' '
361+ return s: arabic2roman (s: roman2arabic (a: bullet .bullet) + 1 , l: islower )
338362endfun
339363
340364fun ! s: next_abc_bullet (bullet)
341365 let l: islower = a: bullet .bullet == # tolower (a: bullet .bullet)
342- let l: next_num = s: dec2abc (s: abc2dec (a: bullet .bullet) + 1 , l: islower )
343- return a: bullet .leading_space . l: next_num . a: bullet .closure . ' '
366+ return s: dec2abc (s: abc2dec (a: bullet .bullet) + 1 , l: islower )
344367endfun
345368
346369fun ! s: next_num_bullet (bullet)
347- let l: next_num = a: bullet .bullet + 1
348- return a: bullet .leading_space . l: next_num . a: bullet .closure . ' '
370+ return a: bullet .bullet + 1
349371endfun
350372
351373fun ! s: next_chk_bullet (bullet)
352- return a: bullet .leading_space . ' - [ ] '
374+ return ' - [ ]'
353375endfun
354376" }}}
355377
@@ -363,7 +385,8 @@ endfun
363385fun ! s: insert_new_bullet ()
364386 let l: curr_line_num = line (' .' )
365387 let l: next_line_num = l: curr_line_num + g: bullets_line_spacing
366- let l: closest_bullet_types = s: closest_bullet_types (l: curr_line_num )
388+ let l: curr_indent = indent (l: curr_line_num )
389+ let l: closest_bullet_types = s: closest_bullet_types (l: curr_line_num , l: curr_indent )
367390 let l: bullet = s: resolve_bullet_type (l: closest_bullet_types )
368391 " need to find which line starts the previous bullet started at and start
369392 " searching up from there
@@ -573,6 +596,137 @@ endfun
573596command ! -range =% RenumberSelection call <SID> renumber_selection ()
574597" --------------------------------------------------------- }}}
575598
599+ " Changing outline level ---------------------------------- {{{
600+ fun ! s: change_bullet_level (direction)
601+ let l: lnum = line (' .' )
602+ let l: curr_line = s: parse_bullet (l: lnum , getline (l: lnum ))
603+
604+ if a: direction == 1
605+ if l: curr_line != [] && indent (l: lnum ) == 0
606+ " Promoting a bullet at the highest level will delete the bullet
607+ call setline (l: lnum , l: curr_line [0 ].text_after_bullet)
608+ else
609+ execute " normal! a\<C-d> "
610+ endif
611+ else
612+ execute " normal! a\<C-t> "
613+ endif
614+
615+ let l: curr_indent = indent (l: lnum )
616+ let l: curr_bullet= s: closest_bullet_types (l: lnum , l: curr_indent )
617+ let l: curr_bullet = s: resolve_bullet_type (l: curr_bullet )
618+
619+ if l: curr_bullet == {}
620+ " Only change the bullet level if it's currently a bullet.
621+ return
622+ endif
623+
624+ let l: curr_line = l: curr_bullet .starting_at_line_num
625+ let l: closest_bullet = s: closest_bullet_types (l: curr_line - g: bullets_line_spacing , l: curr_indent )
626+ let l: closest_bullet = s: resolve_bullet_type (l: closest_bullet )
627+
628+ if l: closest_bullet == {}
629+ " If there is no parent/sibling bullet then this bullet shouldn't change.
630+ return
631+ endif
632+
633+ let l: islower = l: closest_bullet .bullet == # tolower (l: closest_bullet .bullet)
634+ let l: closest_type = l: islower ? l: closest_bullet .bullet_type :
635+ \ toupper (l: closest_bullet .bullet_type)
636+
637+ if l: closest_bullet .bullet_type == # ' std'
638+ " Append the bullet marker to the type, e.g., 'std*'
639+
640+ let l: closest_type = l: closest_type . l: closest_bullet .bullet
641+ endif
642+
643+ let l: closest_index = index (g: bullets_outline_levels , l: closest_type )
644+
645+ if l: closest_index == -1
646+ " We are in a list using markers that aren't specified in
647+ " g:bullets_outline_levels so we shouldn't try to change the current
648+ " bullet.
649+ return
650+ endif
651+
652+ let l: closest_indent = indent (l: closest_bullet .starting_at_line_num)
653+
654+ if (l: curr_indent == l: closest_indent )
655+ " The closest bullet is a sibling so the current bullet should
656+ " increment to the next bullet marker.
657+
658+ let l: next_bullet = s: next_bullet_str (l: closest_bullet )
659+ let l: next_bullet_str = s: pad_to_length (l: next_bullet , l: closest_bullet .bullet_length)
660+ \ . l: curr_bullet .text_after_bullet
661+
662+ elseif l: closest_index + 1 < len (g: bullets_outline_levels ) || l: curr_indent < l: closest_indent
663+ " The current bullet is a child of the closest bullet so figure out
664+ " what bullet type it should have and set its marker to the first
665+ " character of that type.
666+
667+ let l: next_index = l: closest_index + 1
668+ let l: next_type = g: bullets_outline_levels [l: next_index ]
669+ let l: next_islower = l: next_type == # tolower (l: next_type )
670+ let l: trailing_space = ' '
671+
672+ let l: curr_bullet .closure = l: closest_bullet .closure
673+
674+ " set the bullet marker to the first character of that type
675+ if l: next_type == ? ' rom'
676+ let l: next_num = s: arabic2roman (1 , l: next_islower )
677+ elseif l: next_type == ? ' abc'
678+ let l: next_num = s: dec2abc (1 , l: next_islower )
679+ elseif l: next_type == # ' num'
680+ let l: next_num = ' 1'
681+ else
682+ " standard bullet; l:next_type contains the bullet symbol to use
683+ let l: next_num = strpart (l: next_type , len (l: next_type ) - 1 )
684+ let l: curr_bullet .closure = ' '
685+ endif
686+
687+ let l: next_bullet_str =
688+ \ l: curr_bullet .leading_space
689+ \ . l: next_num
690+ \ . l: curr_bullet .closure
691+ \ . l: trailing_space
692+ \ . l: curr_bullet .text_after_bullet
693+
694+ else
695+ " We're outside of the defined outline levels
696+ let l: next_bullet_str =
697+ \ l: curr_bullet .leading_space
698+ \ . l: curr_bullet .text_after_bullet
699+ endif
700+
701+ " Apply the new bullet
702+ if l: next_bullet_str !=# ' '
703+ call setline (l: lnum , l: next_bullet_str )
704+ execute ' normal! $'
705+
706+ elseif g: bullets_delete_last_bullet_if_empty
707+ let l: orig_line = s: parse_bullet (l: lnum , getline (l: lnum ))
708+ if l: orig_line != []
709+ call setline (l: lnum , l: orig_line [0 ].leading_space . l: orig_line [0 ].text_after_bullet)
710+ endif
711+ endif
712+
713+ return
714+ endfun
715+
716+ fun ! s: bullet_demote ()
717+ call s: change_bullet_level (-1 )
718+ endfun
719+
720+ fun ! s: bullet_promote ()
721+ call s: change_bullet_level (1 )
722+ endfun
723+
724+ command ! BulletDemote call <SID> bullet_demote ()
725+ command ! BulletPromote call <SID> bullet_promote ()
726+
727+
728+ " --------------------------------------------------------- }}}
729+
576730" Keyboard mappings --------------------------------------- {{{
577731fun ! s: add_local_mapping (mapping_type, mapping , action)
578732 let l: file_types = join (g: bullets_enabled_file_types , ' ,' )
@@ -613,6 +767,13 @@ augroup TextBulletsMappings
613767
614768 " Toggle checkbox
615769 call s: add_local_mapping (' nnoremap' , ' <leader>x' , ' :ToggleCheckbox<cr>' )
770+
771+ " Promote and Demote outline level
772+ call s: add_local_mapping (' inoremap' , ' <C-t>' , ' <C-o>:call <SID>bullet_demote()<cr>' )
773+ call s: add_local_mapping (' nnoremap' , ' >>' , ' :call <SID>bullet_demote()<cr>' )
774+ call s: add_local_mapping (' inoremap' , ' <C-d>' , ' <C-o>:call <SID>bullet_promote()<cr>' )
775+ call s: add_local_mapping (' nnoremap' , ' <<' , ' :call <SID>bullet_promote()<cr>' )
776+
616777 end
617778augroup END
618779" --------------------------------------------------------- }}}
0 commit comments