changelog.vim 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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. " If 'filetype' isn't "changelog", we must have been to add ChangeLog opener
  31. if &filetype == 'changelog'
  32. if exists('b:did_ftplugin')
  33. finish
  34. endif
  35. let b:did_ftplugin = 1
  36. let s:cpo_save = &cpo
  37. set cpo&vim
  38. " Set up the format used for dates.
  39. if !exists('g:changelog_dateformat')
  40. if exists('g:changelog_timeformat')
  41. let g:changelog_dateformat = g:changelog_timeformat
  42. else
  43. let g:changelog_dateformat = "%Y-%m-%d"
  44. endif
  45. endif
  46. function! s:username()
  47. if exists('g:changelog_username')
  48. return g:changelog_username
  49. elseif $EMAIL != ""
  50. return $EMAIL
  51. elseif $EMAIL_ADDRESS != ""
  52. return $EMAIL_ADDRESS
  53. endif
  54. let s:default_login = 'unknown'
  55. " Disabled by default for security reasons.
  56. if dist#vim#IsSafeExecutable('changelog', 'whoami')
  57. let login = s:login()
  58. else
  59. let login = s:default_login
  60. endif
  61. return printf('%s <%s@%s>', s:name(login), login, s:hostname())
  62. endfunction
  63. function! s:login()
  64. return s:trimmed_system_with_default('whoami', s:default_login)
  65. endfunction
  66. function! s:trimmed_system_with_default(command, default)
  67. return s:first_line(s:system_with_default(a:command, a:default))
  68. endfunction
  69. function! s:system_with_default(command, default)
  70. let output = system(a:command)
  71. if v:shell_error
  72. return a:default
  73. endif
  74. return output
  75. endfunction
  76. function! s:first_line(string)
  77. return substitute(a:string, '\n.*$', "", "")
  78. endfunction
  79. function! s:name(login)
  80. for name in [s:gecos_name(a:login), $NAME, s:capitalize(a:login)]
  81. if name != ""
  82. return name
  83. endif
  84. endfor
  85. endfunction
  86. function! s:gecos_name(login)
  87. for line in s:try_reading_file('/etc/passwd')
  88. if line =~ '^' . a:login . ':'
  89. return substitute(s:passwd_field(line, 5), '&', s:capitalize(a:login), "")
  90. endif
  91. endfor
  92. return ""
  93. endfunction
  94. function! s:try_reading_file(path)
  95. try
  96. return readfile(a:path)
  97. catch
  98. return []
  99. endtry
  100. endfunction
  101. function! s:passwd_field(line, field)
  102. let fields = split(a:line, ':', 1)
  103. if len(fields) < a:field
  104. return ""
  105. endif
  106. return fields[a:field - 1]
  107. endfunction
  108. function! s:capitalize(word)
  109. return toupper(a:word[0]) . strpart(a:word, 1)
  110. endfunction
  111. function! s:hostname()
  112. return s:trimmed_system_with_default('hostname', 'localhost')
  113. endfunction
  114. " Format used for new date entries.
  115. if !exists('g:changelog_new_date_format')
  116. let g:changelog_new_date_format = "%d %u\n\n\t* %p%c\n\n"
  117. endif
  118. " Format used for new entries to current date entry.
  119. if !exists('g:changelog_new_entry_format')
  120. let g:changelog_new_entry_format = "\t* %p%c"
  121. endif
  122. " Regular expression used to find a given date entry.
  123. if !exists('g:changelog_date_entry_search')
  124. let g:changelog_date_entry_search = '^\s*%d\_s*%u'
  125. endif
  126. " Regular expression used to find the end of a date entry
  127. if !exists('g:changelog_date_end_entry_search')
  128. let g:changelog_date_end_entry_search = '^\s*$'
  129. endif
  130. " Substitutes specific items in new date-entry formats and search strings.
  131. " Can be done with substitute of course, but unclean, and need \@! then.
  132. function! s:substitute_items(str, date, user, prefix)
  133. let str = a:str
  134. let middles = {'%': '%', 'd': a:date, 'u': a:user, 'p': a:prefix, 'c': '{cursor}'}
  135. let i = stridx(str, '%')
  136. while i != -1
  137. let inc = 0
  138. if has_key(middles, str[i + 1])
  139. let mid = middles[str[i + 1]]
  140. let str = strpart(str, 0, i) . mid . strpart(str, i + 2)
  141. let inc = strlen(mid) - 1
  142. endif
  143. let i = stridx(str, '%', i + 1 + inc)
  144. endwhile
  145. return str
  146. endfunction
  147. " Position the cursor once we've done all the funky substitution.
  148. function! s:position_cursor()
  149. if search('{cursor}') > 0
  150. let lnum = line('.')
  151. let line = getline(lnum)
  152. let cursor = stridx(line, '{cursor}')
  153. call setline(lnum, substitute(line, '{cursor}', '', ''))
  154. endif
  155. startinsert
  156. endfunction
  157. " Internal function to create a new entry in the ChangeLog.
  158. function! s:new_changelog_entry(prefix)
  159. " Deal with 'paste' option.
  160. let save_paste = &paste
  161. let &paste = 1
  162. call cursor(1, 1)
  163. " Look for an entry for today by our user.
  164. let date = strftime(g:changelog_dateformat)
  165. let search = s:substitute_items(g:changelog_date_entry_search, date,
  166. \ s:username(), a:prefix)
  167. if search(search) > 0
  168. " Ok, now we look for the end of the date entry, and add an entry.
  169. call cursor(nextnonblank(line('.') + 1), 1)
  170. if search(g:changelog_date_end_entry_search, 'W') > 0
  171. let p = (line('.') == line('$')) ? line('.') : line('.') - 1
  172. else
  173. let p = line('.')
  174. endif
  175. let ls = split(s:substitute_items(g:changelog_new_entry_format, '', '', a:prefix),
  176. \ '\n')
  177. call append(p, ls)
  178. call cursor(p + 1, 1)
  179. else
  180. " Flag for removing empty lines at end of new ChangeLogs.
  181. let remove_empty = line('$') == 1
  182. " No entry today, so create a date-user header and insert an entry.
  183. let todays_entry = s:substitute_items(g:changelog_new_date_format,
  184. \ date, s:username(), a:prefix)
  185. " Make sure we have a cursor positioning.
  186. if stridx(todays_entry, '{cursor}') == -1
  187. let todays_entry = todays_entry . '{cursor}'
  188. endif
  189. " Now do the work.
  190. call append(0, split(todays_entry, '\n'))
  191. " Remove empty lines at end of file.
  192. if remove_empty
  193. $-/^\s*$/-1,$delete
  194. endif
  195. " Reposition cursor once we're done.
  196. call cursor(1, 1)
  197. endif
  198. call s:position_cursor()
  199. " And reset 'paste' option
  200. let &paste = save_paste
  201. endfunction
  202. let b:undo_ftplugin = "setl com< fo< et< ai<"
  203. setlocal comments=
  204. setlocal formatoptions+=t
  205. setlocal noexpandtab
  206. setlocal autoindent
  207. if &textwidth == 0
  208. setlocal textwidth=78
  209. let b:undo_ftplugin .= " tw<"
  210. endif
  211. if !exists("no_plugin_maps") && !exists("no_changelog_maps") && exists(":NewChangelogEntry") != 2
  212. nnoremap <buffer> <silent> <Leader>o :<C-u>call <SID>new_changelog_entry('')<CR>
  213. xnoremap <buffer> <silent> <Leader>o :<C-u>call <SID>new_changelog_entry('')<CR>
  214. command! -buffer -nargs=0 NewChangelogEntry call s:new_changelog_entry('')
  215. let b:undo_ftplugin .= " | sil! exe 'nunmap <buffer> <Leader>o'" .
  216. \ " | sil! exe 'vunmap <buffer> <Leader>o'" .
  217. \ " | sil! delc NewChangelogEntry"
  218. endif
  219. let &cpo = s:cpo_save
  220. unlet s:cpo_save
  221. else
  222. let s:cpo_save = &cpo
  223. set cpo&vim
  224. if !exists("no_plugin_maps") && !exists("no_changelog_maps")
  225. " Add the Changelog opening mapping
  226. nnoremap <silent> <Leader>o :call <SID>open_changelog()<CR>
  227. let b:undo_ftplugin .= " | silent! exe 'nunmap <buffer> <Leader>o"
  228. endif
  229. function! s:open_changelog()
  230. let path = expand('%:p:h')
  231. if exists('b:changelog_path')
  232. let changelog = b:changelog_path
  233. else
  234. if exists('b:changelog_name')
  235. let name = b:changelog_name
  236. else
  237. let name = 'ChangeLog'
  238. endif
  239. while isdirectory(path)
  240. let changelog = path . '/' . name
  241. if filereadable(changelog)
  242. break
  243. endif
  244. let parent = substitute(path, '/\+[^/]*$', "", "")
  245. if path == parent
  246. break
  247. endif
  248. let path = parent
  249. endwhile
  250. endif
  251. if !filereadable(changelog)
  252. return
  253. endif
  254. if exists('b:changelog_entry_prefix')
  255. let prefix = call(b:changelog_entry_prefix, [])
  256. else
  257. let prefix = substitute(strpart(expand('%:p'), strlen(path)), '^/\+', "", "")
  258. endif
  259. let buf = bufnr(changelog)
  260. if buf != -1
  261. if bufwinnr(buf) != -1
  262. execute bufwinnr(buf) . 'wincmd w'
  263. else
  264. execute 'sbuffer' buf
  265. endif
  266. else
  267. execute 'split' fnameescape(changelog)
  268. endif
  269. call s:new_changelog_entry(prefix)
  270. endfunction
  271. let &cpo = s:cpo_save
  272. unlet s:cpo_save
  273. endif