@@ -57,7 +57,22 @@ while s:power >= 0
5757endwhile
5858" ------------------------------------------------------ }}}
5959
60- " Bullet type detection ---------------------------------------- {{{
60+ " Parse Bullet Type ------------------------------------------- {{{
61+ fun ! s: parse_bullet (line_num, line_text)
62+ let l: kinds = s: filter (
63+ \ [
64+ \ s: match_bullet_list_item (a: line_text ),
65+ \ s: match_checkbox_bullet_item (a: line_text ),
66+ \ s: match_numeric_list_item (a: line_text ),
67+ \ s: match_roman_list_item (a: line_text ),
68+ \ s: match_alphabetical_list_item (a: line_text ),
69+ \ ],
70+ \ ' !empty(v:val)'
71+ \ )
72+
73+ return s: map (l: kinds , ' extend(v:val, { "starting_at_line_num": ' . a: line_num . ' })' )
74+ endfun
75+
6176fun ! s: match_numeric_list_item (input_text)
6277 let l: num_bullet_regex = ' \v^((\s*)(\d+)(\.|\))(\s+))(.*)'
6378 let l: matches = matchlist (a: input_text , l: num_bullet_regex )
@@ -84,6 +99,7 @@ fun! s:match_numeric_list_item(input_text)
8499 \ }
85100endfun
86101
102+
87103fun ! s: match_roman_list_item (input_text)
88104 let l: rom_bullet_regex = join ([
89105 \ ' \v\C' ,
@@ -126,13 +142,15 @@ fun! s:match_alphabetical_list_item(input_text)
126142 return {}
127143 endif
128144
129- let l: abc_bullet_regex = join ([
145+ let l: max = string (g: bullets_max_alpha_characters )
146+ let l: abc_bullet_regex = join ([
130147 \ ' \v^((\s*)(\u{1,' ,
131- \ string ( g: bullets_max_alpha_characters ) ,
148+ \ l: max ,
132149 \ ' }|\l{1,' ,
133- \ string ( g: bullets_max_alpha_characters ) ,
150+ \ l: max ,
134151 \ ' })(\.|\))(\s+))(.*)' ], ' ' )
135- let l: matches = matchlist (a: input_text , l: abc_bullet_regex )
152+
153+ let l: matches = matchlist (a: input_text , l: abc_bullet_regex )
136154
137155 if empty (l: matches )
138156 return {}
@@ -199,115 +217,156 @@ fun! s:match_bullet_list_item(input_text)
199217 \ ' text_after_bullet' : l: text_after_bullet
200218 \ }
201219endfun
220+ " ------------------------------------------------------- }}}
202221
203- fun ! s: parse_bullet (line_text)
204- let l: std_bullet_matches = s: match_bullet_list_item (a: line_text )
205- let l: chk_bullet_matches = s: match_checkbox_bullet_item (a: line_text )
206- let l: num_bullet_matches = s: match_numeric_list_item (a: line_text )
207- let l: rom_bullet_matches = s: match_roman_list_item (a: line_text )
208- let l: abc_bullet_matches = s: match_alphabetical_list_item (a: line_text )
209-
210- if ! empty (l: chk_bullet_matches )
211- return l: chk_bullet_matches
212- elseif ! empty (l: std_bullet_matches )
213- return l: std_bullet_matches
214- elseif ! empty (l: num_bullet_matches )
215- return l: num_bullet_matches
216- elseif ! empty (l: rom_bullet_matches )
217- return l: rom_bullet_matches
218- elseif ! empty (l: abc_bullet_matches )
219- return l: abc_bullet_matches
220- else
222+ " Resolve Bullet Type ----------------------------------- {{{
223+ fun ! s: closest_bullet_types (from_line_num)
224+ let l: lnum = a: from_line_num
225+ let l: ltxt = getline (l: lnum )
226+ let l: bullet_kinds = s: parse_bullet (l: lnum , l: ltxt )
227+
228+ " Support for wrapped text bullets
229+ " 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
232+ let l: ltxt = getline (l: lnum )
233+ let l: bullet_kinds = s: parse_bullet (l: lnum , l: ltxt )
234+ endwhile
235+
236+ return l: bullet_kinds
237+ endfun
238+
239+ fun ! s: resolve_bullet_type (bullet_types)
240+ if empty (a: bullet_types )
221241 return {}
242+ elseif len (a: bullet_types ) == 2 && s: has_rom_and_abc (a: bullet_types )
243+ return s: resolve_rom_or_abc (a: bullet_types )
244+ elseif len (a: bullet_types ) == 2 && s: has_chk_and_std (a: bullet_types )
245+ return s: resolve_chk_or_std (a: bullet_types )
246+ else
247+ return a: bullet_types [0 ]
222248 endif
223249endfun
224- " ------------------------------------------------------- }}}
225250
226- " Helper methods ---------------------------------------- {{{
227- fun ! s: get_visual_selection_lines ()
228- let [l: lnum1 , l: col1 ] = getpos (" '<" )[1 :2 ]
229- let [l: lnum2 , l: col2 ] = getpos (" '>" )[1 :2 ]
230- let l: lines = getline (l: lnum1 , l: lnum2 )
231- let l: lines [-1 ] = l: lines [-1 ][: l: col2 - (&selection == # ' inclusive' ? 1 : 2 )]
232- let l: lines [0 ] = l: lines [0 ][l: col1 - 1 :]
233- let l: index = l: lnum1
234- let l: lines_with_index = []
235- for l: line in l: lines
236- let l: lines_with_index += [{' text' : l: line , ' nr' : l: index }]
237- let l: index += 1
238- endfor
239- return l: lines_with_index
251+ fun ! s: contains_type (bullet_types, type )
252+ return s: has_item (a: bullet_types , ' v:val.bullet_type ==# "' . a: type . ' "' )
240253endfun
241- " ------------------------------------------------------- }}}
242254
243- " Generate bullets -------------------------------------- {{{
244- fun ! s: pad_to_length (str, len )
245- if g: bullets_pad_right == 0 | return a: str | endif
246- let l: len = a: len - len (a: str )
247- let l: str = a: str
248- if (l: len <= 0 ) | return a: str | endif
249- while l: len > 0
250- let l: str = l: str . ' '
251- let l: len = l: len - 1
252- endwhile
253- return l: str
255+ fun ! s: find_by_type (bullet_types, type )
256+ return s: find (a: bullet_types , ' v:val.bullet_type ==# "' . a: type . ' "' )
257+ endfun
258+
259+ " Roman Numeral vs Alphabetic Bullets ---------------------------------- {{{
260+ fun ! s: resolve_rom_or_abc (bullet_types)
261+ 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 )
264+
265+ if len (l: prev_bullet_types ) == 0
266+
267+ " can't find previous bullet - so we probably have a rom i. bullet
268+ return s: find_by_type (a: bullet_types , ' rom' )
269+
270+ elseif len (l: prev_bullet_types ) == 1 && s: has_rom_or_abc (l: prev_bullet_types )
271+
272+ " previous bullet is conclusive, use it's type to continue
273+ return s: find_by_type (a: bullet_types , l: prev_bullet_types [0 ].bullet_type)
274+
275+ elseif s: has_rom_and_abc (l: prev_bullet_types )
276+
277+ " inconclusive - keep searching up recursively
278+ let l: prev_bullet = s: resolve_rom_or_abc (l: prev_bullet_types )
279+ return s: find_by_type (a: bullet_types , l: prev_bullet .bullet_type)
280+
281+ else
282+
283+ " parent has unrelated bullet type, we'll go with rom
284+ return s: find_by_type (a: bullet_types , ' rom' )
285+
286+ endif
287+ endfun
288+
289+ fun ! s: has_rom_or_abc (bullet_types)
290+ let l: has_rom = s: contains_type (a: bullet_types , ' rom' )
291+ let l: has_abc = s: contains_type (a: bullet_types , ' abc' )
292+ return l: has_rom || l: has_abc
293+ endfun
294+
295+ fun ! s: has_rom_and_abc (bullet_types)
296+ let l: has_rom = s: contains_type (a: bullet_types , ' rom' )
297+ let l: has_abc = s: contains_type (a: bullet_types , ' abc' )
298+ return l: has_rom && l: has_abc
299+ endfun
300+ " ------------------------------------------------------- }}}
301+
302+ " Checkbox vs Standard Bullets ----------------------------------------- {{{
303+ fun ! s: resolve_chk_or_std (bullet_types)
304+ " if it matches both regular and checkbox it is most likely a checkbox
305+ return s: find_by_type (a: bullet_types , ' chk' )
306+ endfun
307+
308+ fun ! s: has_chk_and_std (bullet_types)
309+ let l: has_chk = s: contains_type (a: bullet_types , ' chk' )
310+ let l: has_std = s: contains_type (a: bullet_types , ' std' )
311+ return l: has_chk && l: has_std
254312endfun
313+ " ------------------------------------------------------- }}}
255314
315+ " ------------------------------------------------------- }}}
316+
317+ " Build Next Bullet -------------------------------------- {{{
256318fun ! s: next_bullet_str (bullet)
257- if a: bullet .bullet_type == # ' rom'
258- let l: islower = a: bullet .bullet == # tolower (a: bullet .bullet)
259- let l: next_num = s: arabic2roman (s: roman2arabic (a: bullet .bullet) + 1 , l: islower )
260- return a: bullet .leading_space . l: next_num . a: bullet .closure . ' '
261- elseif a: bullet .bullet_type == # ' abc'
262- let l: islower = a: bullet .bullet == # tolower (a: bullet .bullet)
263- let l: next_num = s: dec2abc (s: abc2dec (a: bullet .bullet) + 1 , l: islower )
264- return a: bullet .leading_space . l: next_num . a: bullet .closure . ' '
265- elseif a: bullet .bullet_type == # ' num'
266- let l: next_num = a: bullet .bullet + 1
267- return a: bullet .leading_space . l: next_num . a: bullet .closure . ' '
268- elseif a: bullet .bullet_type == # ' chk'
269- return a: bullet .leading_space . ' - [ ] '
319+ let l: bullet_type = get (a: bullet , ' bullet_type' )
320+
321+ if l: bullet_type == # ' rom'
322+ return s: next_rom_bullet (a: bullet )
323+ elseif l: bullet_type == # ' abc'
324+ return s: next_abc_bullet (a: bullet )
325+ elseif l: bullet_type == # ' num'
326+ return s: next_num_bullet (a: bullet )
327+ elseif l: bullet_type == # ' chk'
328+ return s: next_chk_bullet (a: bullet )
270329 else
271330 return a: bullet .whole_bullet
272331 endif
273332endfun
274333
275- fun ! s: delete_empty_bullet (line_num )
276- if g: bullets_delete_last_bullet_if_empty
277- call setline ( a: line_num , ' ' )
278- endif
334+ fun ! s: next_rom_bullet (bullet )
335+ 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 . ' '
279338endfun
280339
281- fun ! s: indented (line_text)
282- return a: line_text = ~# ' \v^\s+\w'
340+ fun ! s: next_abc_bullet (bullet)
341+ 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 . ' '
283344endfun
284345
285- fun ! s: detect_bullet_line (from_line_num )
286- let l: lnum = a: from_line_num
287- let l: ltxt = getline ( l: lnum )
288- let l: bullet = s: parse_bullet ( l: ltxt )
346+ fun ! 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 . ' '
349+ endfun
289350
290- while l: lnum > 1 && s: indented (l: ltxt ) && l: bullet == {}
291- let l: lnum = l: lnum - 1
292- let l: ltxt = getline (l: lnum )
293- let l: bullet = s: parse_bullet (l: ltxt )
294- endwhile
351+ fun ! s: next_chk_bullet (bullet)
352+ return a: bullet .leading_space . ' - [ ] '
353+ endfun
354+ " }}}
295355
296- return l: bullet
356+ " Generate bullets -------------------------------------- {{{
357+ fun ! s: delete_empty_bullet (line_num)
358+ if g: bullets_delete_last_bullet_if_empty
359+ call setline (a: line_num , ' ' )
360+ endif
297361endfun
298362
299363fun ! s: insert_new_bullet ()
300364 let l: curr_line_num = line (' .' )
301365 let l: next_line_num = l: curr_line_num + g: bullets_line_spacing
302- let l: bullet = s: detect_bullet_line (l: curr_line_num )
303- if l: bullet != {} && l: curr_line_num > 1 &&
304- \ (l: bullet .bullet_type == # ' rom' || l: bullet .bullet_type == # ' abc' )
305- let l: bullet_prev = s: detect_bullet_line (l: curr_line_num - 1 )
306- if l: bullet_prev != {} && l: bullet .bullet_type == # ' rom' &&
307- \ (s: roman2arabic (l: bullet .bullet) != (s: roman2arabic (l: bullet_prev .bullet) + 1 ))
308- let l: bullet .bullet_type = ' abc'
309- endif
310- endif
366+ let l: closest_bullet_types = s: closest_bullet_types (l: curr_line_num )
367+ let l: bullet = s: resolve_bullet_type (l: closest_bullet_types )
368+ " need to find which line starts the previous bullet started at and start
369+ " searching up from there
311370 let l: send_return = 1
312371 let l: normal_mode = mode () == # ' n'
313372
@@ -321,15 +380,15 @@ fun! s:insert_new_bullet()
321380 call s: delete_empty_bullet (l: curr_line_num )
322381 elseif ! (l: bullet .bullet_type == # ' abc' && s: abc2dec (l: bullet .bullet) + 1 > s: abc_max )
323382
324- let l: next_bullet_list = [s: pad_to_length (s: next_bullet_str (l: bullet ), l: bullet .bullet_length)]
383+ let l: next_bullet = s: next_bullet_str (l: bullet )
384+ let l: next_bullet_list = [s: pad_to_length (l: next_bullet , l: bullet .bullet_length)]
325385
326386 " prepend blank lines if desired
327387 if g: bullets_line_spacing > 1
328388 let l: next_bullet_list += map (range (g: bullets_line_spacing - 1 ), ' ""' )
329389 call reverse (l: next_bullet_list )
330390 endif
331391
332-
333392 " insert next bullet
334393 call append (l: curr_line_num , l: next_bullet_list )
335394 " got to next line after the new bullet
@@ -406,9 +465,8 @@ command! ToggleCheckbox call <SID>toggle_checkbox()
406465" Roman numerals --------------------------------------------- {{{
407466
408467" Roman numeral functions lifted from tpope's speeddating.vim
409- " where they are in turn
410- " based on similar functions from VisIncr.vim
411- "
468+ " where they are in turn based on similar functions from VisIncr.vim
469+
412470let s: a2r = [
413471 \ [1000 , ' m' ], [900 , ' cm' ], [500 , ' d' ], [400 , ' cd' ],
414472 \ [100 , ' c' ], [90 , ' xc' ], [50 , ' l' ], [40 , ' xl' ],
@@ -457,7 +515,6 @@ endfunction
457515
458516" Alphabetic ordinal functions
459517" Treat alphabetic ordinals as base-26 numbers to make things easy
460- "
461518fun ! s: abc2dec (abc )
462519 let l: abc = tolower (a: abc )
463520 let l: dec = char2nr (l: abc [0 ]) - char2nr (' a' ) + 1
@@ -593,6 +650,67 @@ augroup TextBulletsMappings
593650augroup END
594651" --------------------------------------------------------- }}}
595652
653+ " Helpers ----------------------------------------------- {{{
654+ fun ! s: get_visual_selection_lines ()
655+ let [l: lnum1 , l: col1 ] = getpos (" '<" )[1 :2 ]
656+ let [l: lnum2 , l: col2 ] = getpos (" '>" )[1 :2 ]
657+ let l: lines = getline (l: lnum1 , l: lnum2 )
658+ let l: lines [-1 ] = l: lines [-1 ][: l: col2 - (&selection == # ' inclusive' ? 1 : 2 )]
659+ let l: lines [0 ] = l: lines [0 ][l: col1 - 1 :]
660+ let l: index = l: lnum1
661+ let l: lines_with_index = []
662+ for l: line in l: lines
663+ let l: lines_with_index += [{' text' : l: line , ' nr' : l: index }]
664+ let l: index += 1
665+ endfor
666+ return l: lines_with_index
667+ endfun
668+
669+ fun ! s: pad_to_length (str, len )
670+ if g: bullets_pad_right == 0 | return a: str | endif
671+ let l: len = a: len - len (a: str )
672+ let l: str = a: str
673+ if (l: len <= 0 ) | return a: str | endif
674+ while l: len > 0
675+ let l: str = l: str . ' '
676+ let l: len = l: len - 1
677+ endwhile
678+ return l: str
679+ endfun
680+
681+ fun ! s: is_indented (line_text)
682+ return a: line_text = ~# ' \v^\s+\w'
683+ endfun
684+
685+ fun ! s: map (list , fn)
686+ let new_list = deepcopy (a: list )
687+ call map (new_list, a: fn )
688+ return new_list
689+ endfun
690+
691+ fun ! s: filter (list , fn)
692+ let new_list = deepcopy (a: list )
693+ call filter (new_list, a: fn )
694+ return new_list
695+ endfun
696+
697+ fun ! s: find (list , fn)
698+ let l: fn = substitute (a: fn , ' v:val' , ' l:item' , ' g' )
699+ for l: item in a: list
700+ let l: new_item = deepcopy (l: item )
701+ if execute (' echon (' . l: fn . ' )' ) == # ' 1'
702+ return l: new_item
703+ endif
704+ endfor
705+
706+ return 0
707+ endfun
708+
709+ fun ! s: has_item (list , fn)
710+ return ! empty (s: find (a: list , a: fn ))
711+ endfun
712+ " ------------------------------------------------------- }}}
713+
596714" Restore previous external compatibility options --------- {{{
597715let &cpoptions = s: save_cpo
598716" -------------------------------------------------------- }}}
0 commit comments