changelog.vim 8.7 KB

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