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