Skip to content

Commit fd9ce9d

Browse files
authored
Merge pull request #50 from dkarter/fix-alphabetic-lists
Fix Alphabetic vs Roman Numeral Bullets Bug
2 parents e0b1f8f + 33f2e3b commit fd9ce9d

File tree

2 files changed

+244
-96
lines changed

2 files changed

+244
-96
lines changed

plugin/bullets.vim

Lines changed: 212 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,22 @@ while s:power >= 0
5757
endwhile
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+
6176
fun! 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
\ }
85100
endfun
86101

102+
87103
fun! 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
\ }
201219
endfun
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
223249
endfun
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 . '"')
240253
endfun
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
254312
endfun
313+
" ------------------------------------------------------- }}}
255314

315+
" ------------------------------------------------------- }}}
316+
317+
" Build Next Bullet -------------------------------------- {{{
256318
fun! 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
273332
endfun
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 . ' '
279338
endfun
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 . ' '
283344
endfun
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
297361
endfun
298362

299363
fun! 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+
412470
let 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-
"
461518
fun! 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
593650
augroup 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 --------- {{{
597715
let &cpoptions = s:save_cpo
598716
" -------------------------------------------------------- }}}

0 commit comments

Comments
 (0)