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