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:      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
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  if exists(":NewChangelogEntry") != 2
227    nnoremap <buffer> <silent> <Leader>o :<C-u>call <SID>new_changelog_entry('')<CR>
228    xnoremap <buffer> <silent> <Leader>o :<C-u>call <SID>new_changelog_entry('')<CR>
229    command! -nargs=0 NewChangelogEntry call s:new_changelog_entry('')
230  endif
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  let &cpo = s:cpo_save
245  unlet s:cpo_save
246else
247  let s:cpo_save = &cpo
248  set cpo&vim
249
250  " Add the Changelog opening mapping
251  nnoremap <silent> <Leader>o :call <SID>open_changelog()<CR>
252
253  function! s:open_changelog()
254    let path = expand('%:p:h')
255    if exists('b:changelog_path')
256      let changelog = b:changelog_path
257    else
258      if exists('b:changelog_name')
259        let name = b:changelog_name
260      else
261        let name = 'ChangeLog'
262      endif
263      while isdirectory(path)
264        let changelog = path . '/' . name
265        if filereadable(changelog)
266          break
267        endif
268        let parent = substitute(path, '/\+[^/]*$', "", "")
269        if path == parent
270          break
271        endif
272        let path = parent
273      endwhile
274    endif
275    if !filereadable(changelog)
276      return
277    endif
278
279    if exists('b:changelog_entry_prefix')
280      let prefix = call(b:changelog_entry_prefix, [])
281    else
282      let prefix = substitute(strpart(expand('%:p'), strlen(path)), '^/\+', "", "")
283    endif
284
285    let buf = bufnr(changelog)
286    if buf != -1
287      if bufwinnr(buf) != -1
288        execute bufwinnr(buf) . 'wincmd w'
289      else
290        execute 'sbuffer' buf
291      endif
292    else
293      execute 'split' fnameescape(changelog)
294    endif
295
296    call s:new_changelog_entry(prefix)
297  endfunction
298
299  let &cpo = s:cpo_save
300  unlet s:cpo_save
301endif
302