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