neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

changelog.vim (9507B)


      1 " Vim filetype plugin file
      2 " Language:             generic Changelog file
      3 " Maintainer:           Martin Florian <marfl@posteo.de>
      4 " Previous Maintainer:  Nikolai Weibull <now@bitwi.se>
      5 " Latest Revision:      2021-10-17
      6 " Variables:
      7 "   g:changelog_timeformat (deprecated: use g:changelog_dateformat instead) -
      8 "       description: the timeformat used in ChangeLog entries.
      9 "       default: "%Y-%m-%d".
     10 "   g:changelog_dateformat -
     11 "       description: the format sent to strftime() to generate a date string.
     12 "       default: "%Y-%m-%d".
     13 "   g:changelog_username -
     14 "       description: the username to use in ChangeLog entries
     15 "       default: try to deduce it from environment variables and system files.
     16 " Local Mappings:
     17 "   <Leader>o -
     18 "       adds a new changelog entry for the current user for the current date.
     19 " Global Mappings:
     20 "   <Leader>o -
     21 "       switches to the ChangeLog buffer opened for the current directory, or
     22 "       opens it in a new buffer if it exists in the current directory.  Then
     23 "       it does the same as the local <Leader>o described above.
     24 " Notes:
     25 "   run 'runtime ftplugin/changelog.vim' to enable the global mapping for
     26 "   changelog files.
     27 " TODO:
     28 "  should we perhaps open the ChangeLog file even if it doesn't exist already?
     29 "  Problem is that you might end up with ChangeLog files all over the place.
     30 
     31 " If 'filetype' isn't "changelog", we must have been to add ChangeLog opener
     32 if &filetype == 'changelog'
     33  if exists('b:did_ftplugin')
     34    finish
     35  endif
     36  let b:did_ftplugin = 1
     37 
     38  let s:cpo_save = &cpo
     39  set cpo&vim
     40 
     41  " Set up the format used for dates.
     42  if !exists('g:changelog_dateformat')
     43    if exists('g:changelog_timeformat')
     44      let g:changelog_dateformat = g:changelog_timeformat
     45    else
     46      let g:changelog_dateformat = "%Y-%m-%d"
     47    endif
     48  endif
     49 
     50  function! s:username()
     51    if exists('g:changelog_username')
     52      return g:changelog_username
     53    elseif $EMAIL != ""
     54      return $EMAIL
     55    elseif $EMAIL_ADDRESS != ""
     56      return $EMAIL_ADDRESS
     57    endif
     58    let s:default_login = 'unknown'
     59 
     60    " Disabled by default for security reasons.
     61    if dist#vim#IsSafeExecutable('changelog', 'whoami')
     62      let login = s:login()
     63    else
     64      let login = s:default_login
     65    endif
     66    return printf('%s <%s@%s>', s:name(login), login, s:hostname())
     67  endfunction
     68 
     69  function! s:login()
     70    return s:trimmed_system_with_default('whoami', s:default_login)
     71  endfunction
     72 
     73  function! s:trimmed_system_with_default(command, default)
     74    return s:first_line(s:system_with_default(a:command, a:default))
     75  endfunction
     76 
     77  function! s:system_with_default(command, default)
     78    let output = system(a:command)
     79    if v:shell_error
     80      return a:default
     81    endif
     82    return output
     83  endfunction
     84 
     85  function! s:first_line(string)
     86    return substitute(a:string, '\n.*$', "", "")
     87  endfunction
     88 
     89  function! s:name(login)
     90    for name in [s:gecos_name(a:login), $NAME, s:capitalize(a:login)]
     91      if name != ""
     92        return name
     93      endif
     94    endfor
     95  endfunction
     96 
     97  function! s:gecos_name(login)
     98    for line in s:try_reading_file('/etc/passwd')
     99      if line =~ '^' . a:login . ':'
    100        return substitute(s:passwd_field(line, 5), '&', s:capitalize(a:login), "")
    101      endif
    102    endfor
    103    return ""
    104  endfunction
    105 
    106  function! s:try_reading_file(path)
    107    try
    108      return readfile(a:path)
    109    catch
    110      return []
    111    endtry
    112  endfunction
    113 
    114  function! s:passwd_field(line, field)
    115    let fields = split(a:line, ':', 1)
    116    if len(fields) < a:field
    117      return ""
    118    endif
    119    return fields[a:field - 1]
    120  endfunction
    121 
    122  function! s:capitalize(word)
    123    return toupper(a:word[0]) . strpart(a:word, 1)
    124  endfunction
    125 
    126  function! s:hostname()
    127    return s:trimmed_system_with_default('hostname', 'localhost')
    128  endfunction
    129 
    130  " Format used for new date entries.
    131  if !exists('g:changelog_new_date_format')
    132    let g:changelog_new_date_format = "%d  %u\n\n\t* %p%c\n\n"
    133  endif
    134 
    135  " Format used for new entries to current date entry.
    136  if !exists('g:changelog_new_entry_format')
    137    let g:changelog_new_entry_format = "\t* %p%c"
    138  endif
    139 
    140  " Regular expression used to find a given date entry.
    141  if !exists('g:changelog_date_entry_search')
    142    let g:changelog_date_entry_search = '^\s*%d\_s*%u'
    143  endif
    144 
    145  " Regular expression used to find the end of a date entry
    146  if !exists('g:changelog_date_end_entry_search')
    147    let g:changelog_date_end_entry_search = '^\s*$'
    148  endif
    149 
    150 
    151  " Substitutes specific items in new date-entry formats and search strings.
    152  " Can be done with substitute of course, but unclean, and need \@! then.
    153  function! s:substitute_items(str, date, user, prefix)
    154    let str = a:str
    155    let middles = {'%': '%', 'd': a:date, 'u': a:user, 'p': a:prefix, 'c': '{cursor}'}
    156    let i = stridx(str, '%')
    157    while i != -1
    158      let inc = 0
    159      if has_key(middles, str[i + 1])
    160        let mid = middles[str[i + 1]]
    161        let str = strpart(str, 0, i) . mid . strpart(str, i + 2)
    162        let inc = strlen(mid) - 1
    163      endif
    164      let i = stridx(str, '%', i + 1 + inc)
    165    endwhile
    166    return str
    167  endfunction
    168 
    169  " Position the cursor once we've done all the funky substitution.
    170  function! s:position_cursor()
    171    if search('{cursor}') > 0
    172      let lnum = line('.')
    173      let line = getline(lnum)
    174      let cursor = stridx(line, '{cursor}')
    175      call setline(lnum, substitute(line, '{cursor}', '', ''))
    176    endif
    177    startinsert
    178  endfunction
    179 
    180  " Internal function to create a new entry in the ChangeLog.
    181  function! s:new_changelog_entry(prefix)
    182    " Deal with 'paste' option.
    183    let save_paste = &paste
    184    let &paste = 1
    185    call cursor(1, 1)
    186    " Look for an entry for today by our user.
    187    let date = strftime(g:changelog_dateformat)
    188    let search = s:substitute_items(g:changelog_date_entry_search, date,
    189                                  \ s:username(), a:prefix)
    190    if search(search) > 0
    191      " Ok, now we look for the end of the date entry, and add an entry.
    192      call cursor(nextnonblank(line('.') + 1), 1)
    193      if search(g:changelog_date_end_entry_search, 'W') > 0
    194 let p = (line('.') == line('$')) ? line('.') : line('.') - 1
    195      else
    196        let p = line('.')
    197      endif
    198      let ls = split(s:substitute_items(g:changelog_new_entry_format, '', '', a:prefix),
    199                   \ '\n')
    200      call append(p, ls)
    201      call cursor(p + 1, 1)
    202    else
    203      " Flag for removing empty lines at end of new ChangeLogs.
    204      let remove_empty = line('$') == 1
    205 
    206      " No entry today, so create a date-user header and insert an entry.
    207      let todays_entry = s:substitute_items(g:changelog_new_date_format,
    208                                          \ date, s:username(), a:prefix)
    209      " Make sure we have a cursor positioning.
    210      if stridx(todays_entry, '{cursor}') == -1
    211        let todays_entry = todays_entry . '{cursor}'
    212      endif
    213 
    214      " Now do the work.
    215      call append(0, split(todays_entry, '\n'))
    216 
    217      " Remove empty lines at end of file.
    218      if remove_empty
    219        $-/^\s*$/-1,$delete
    220      endif
    221 
    222      " Reposition cursor once we're done.
    223      call cursor(1, 1)
    224    endif
    225 
    226    call s:position_cursor()
    227 
    228    " And reset 'paste' option
    229    let &paste = save_paste
    230  endfunction
    231 
    232  let b:undo_ftplugin = "setl com< fo< et< ai<"
    233 
    234  setlocal comments=
    235  setlocal formatoptions+=t
    236  setlocal noexpandtab
    237  setlocal autoindent
    238 
    239  if &textwidth == 0
    240    setlocal textwidth=78
    241    let b:undo_ftplugin .= " tw<"
    242  endif
    243 
    244  if !exists("no_plugin_maps") && !exists("no_changelog_maps") && exists(":NewChangelogEntry") != 2
    245    nnoremap <buffer> <silent> <Leader>o :<C-u>call <SID>new_changelog_entry('')<CR>
    246    xnoremap <buffer> <silent> <Leader>o :<C-u>call <SID>new_changelog_entry('')<CR>
    247    command! -buffer -nargs=0 NewChangelogEntry call s:new_changelog_entry('')
    248    let b:undo_ftplugin .= " | sil! exe 'nunmap <buffer> <Leader>o'" .
    249          \                " | sil! exe 'vunmap <buffer> <Leader>o'" .
    250          \                " | sil! delc NewChangelogEntry"
    251  endif
    252 
    253  let &cpo = s:cpo_save
    254  unlet s:cpo_save
    255 else
    256  let s:cpo_save = &cpo
    257  set cpo&vim
    258 
    259  if !exists("no_plugin_maps") && !exists("no_changelog_maps")
    260    " Add the Changelog opening mapping
    261    nnoremap <silent> <Leader>o :call <SID>open_changelog()<CR>
    262    let b:undo_ftplugin .= " | silent! exe 'nunmap <buffer> <Leader>o"
    263  endif
    264 
    265  function! s:open_changelog()
    266    let path = expand('%:p:h')
    267    if exists('b:changelog_path')
    268      let changelog = b:changelog_path
    269    else
    270      if exists('b:changelog_name')
    271        let name = b:changelog_name
    272      else
    273        let name = 'ChangeLog'
    274      endif
    275      while isdirectory(path)
    276        let changelog = path . '/' . name
    277        if filereadable(changelog)
    278          break
    279        endif
    280        let parent = substitute(path, '/\+[^/]*$', "", "")
    281        if path == parent
    282          break
    283        endif
    284        let path = parent
    285      endwhile
    286    endif
    287    if !filereadable(changelog)
    288      return
    289    endif
    290 
    291    if exists('b:changelog_entry_prefix')
    292      let prefix = call(b:changelog_entry_prefix, [])
    293    else
    294      let prefix = substitute(strpart(expand('%:p'), strlen(path)), '^/\+', "", "")
    295    endif
    296 
    297    let buf = bufnr(changelog)
    298    if buf != -1
    299      if bufwinnr(buf) != -1
    300        execute bufwinnr(buf) . 'wincmd w'
    301      else
    302        execute 'sbuffer' buf
    303      endif
    304    else
    305      execute 'split' fnameescape(changelog)
    306    endif
    307 
    308    call s:new_changelog_entry(prefix)
    309  endfunction
    310 
    311  let &cpo = s:cpo_save
    312  unlet s:cpo_save
    313 endif