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