├── .gitignore ├── LICENSE ├── plugin └── lh.vim ├── README.md ├── doc └── localhistory.txt └── autoload └── lh.vim /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | doc/tags 3 | *.sw* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 mg979 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /plugin/lh.vim: -------------------------------------------------------------------------------- 1 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 2 | " vim-localhistory 3 | " Copyright (C) 2018 Gianmaria Bajo 4 | " License: MIT License 5 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 6 | 7 | if exists('g:loaded_localhistory') 8 | finish 9 | endif 10 | let g:loaded_localhistory = 1 11 | 12 | " Preserve external compatibility options, then enable full vim compatibility 13 | let s:save_cpo = &cpo 14 | set cpo&vim 15 | 16 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 17 | " Init 18 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 19 | 20 | if has('win32') 21 | let g:lh_basedir = get(g:, 'lh_basedir', '~/local_history') 22 | else 23 | let g:lh_basedir = get(g:, 'lh_basedir', '~/.local_history') 24 | endif 25 | 26 | let g:lh_open_mode = get(g:, 'lh_open_mode', 'edit') 27 | let g:lh_vert_diff = get(g:, 'lh_vert_diff', 1) 28 | let g:lh_autobackup_frequency = get(g:, 'lh_autobackup_frequency', 0) 29 | let g:lh_autobackup_first = get(g:, 'lh_autobackup_first', 0) 30 | let g:lh_autobackup_size = get(g:, 'lh_autobackup_size', 10240) 31 | 32 | 33 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 34 | " Commands 35 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 36 | 37 | command! -nargs=? LHWrite call lh#backup_file() 38 | 39 | command! LHLoad call lh#fzf('all', 'Local History') 40 | command! LHLoadTimestamp call lh#fzf('timestamped', 'Timestamped') 41 | command! LHLoadSnapshot call lh#fzf('snapshots', 'Snapshots') 42 | command! LHDiff call lh#fzf('diff', 'Diff with backup') 43 | command! LHDelete call lh#fzf('delete', 'Delete Backups') 44 | command! LHBrowse call lh#browse() 45 | 46 | augroup plugin-lh 47 | autocmd! 48 | autocmd BufEnter * call lh#bufenter() 49 | 50 | if g:lh_autobackup_frequency 51 | autocmd BufWritePost * call lh#auto_backup() 52 | endif 53 | augroup END 54 | 55 | 56 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 57 | " Mappings 58 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 59 | 60 | fun! s:mapkeys(keys, cmd, ...) 61 | let k = g:lh_mappings_prefix . a:keys 62 | let s = a:0 ? '' : '' 63 | if maparg(k, 'n') == '' && !hasmapto(a:cmd) 64 | execute 'nnoremap' s k a:cmd 65 | endif 66 | endfun 67 | 68 | if exists('g:lh_mappings_prefix') && !empty(g:lh_mappings_prefix) 69 | call s:mapkeys('l', ':LHLoad') 70 | call s:mapkeys('d', ':LHDiff') 71 | call s:mapkeys('t', ':LHLoadTimestamp') 72 | call s:mapkeys('s', ':LHLoadSnapshot') 73 | call s:mapkeys('w', ':LHWrite', 1) 74 | call s:mapkeys('', ':LHWrite') 75 | call s:mapkeys('x', ':LHDelete') 76 | call s:mapkeys('b', ':LHBrowse') 77 | call s:mapkeys('?', ':nmap ' . g:lh_mappings_prefix . '') 78 | endif 79 | 80 | " Restore previous external compatibility options 81 | let &cpo = s:save_cpo 82 | unlet s:save_cpo 83 | 84 | " vim: et sw=4 ts=4 sts=4 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Vim Local History 2 | 3 | 4 | #### Introduction 5 | 6 | This plugin creates automatic and/or manual backups for your files, in 7 | predefined paths. Backups are browsed with fzf or built-in `:Explore`. 8 | 9 | Features: 10 | 11 | * automatic backup directories generation, mirroring actual file path 12 | * save backups with timestamp or name (snapshots) 13 | * load/delete backups with fzf 14 | * perform diffs 15 | * optional autobackup on first load (configurable max size) 16 | * optional autobackup on save, if no recent backups are found (configurable) 17 | 18 | 19 | 20 | #### Requirements 21 | 22 | [fzf](https://github.com/junegunn/fzf) is required for many commands. 23 | 24 | 25 | 26 | 27 | #### Installation 28 | 29 | Use [vim-plug](https://github.com/junegunn/vim-plug) or any other Vim plugin manager. 30 | 31 | With vim-plug: 32 | 33 | Plug 'mg979/vim-localhistory' 34 | 35 | 36 | 37 | #### Usage 38 | 39 | Default base directory is `~/.local_history` (Unix) or `~\local_history` (Windows): 40 | 41 | let g:lh_basedir = '~/.local_history' 42 | 43 | When saving a backup, the file will mirror the original path inside the base dir: 44 | 45 | /path/to/file -> ~/.local_history/path/to/file 46 | 47 | To enable mappings, specify a prefix, for example: 48 | 49 | let g:lh_mappings_prefix = 'gh' 50 | 51 | Change the position in which files are opened (eg. `vs`, `sp`, `edit`, `botright` `vs`, etc): 52 | 53 | let g:lh_open_mode = 'bo vs' 54 | 55 | Set to 0 if you prefer horizontal split for diff windows: 56 | 57 | let g:lh_vert_diff = 0 58 | 59 | 60 | 61 | #### Autobackup 62 | 63 | To enable autobackup, you must first set these variables. It will only work 64 | after Vim has been restarted. Max size affects both types of autobackup. 65 | 66 | Activate autobackup on first access to a file, and max size (default 10240 bytes): 67 | 68 | let g:lh_autobackup_first = 1 69 | let g:lh_autobackup_size = 51200 70 | 71 | Activate autobackup after save, if no recent backup is found (frequency in minutes): 72 | 73 | let g:lh_autobackup_frequency = 60 74 | 75 | 76 | 77 | 78 | #### Commands 79 | 80 | 81 | |Command | Map | | 82 | |----------------------|-----------------------|----------------------------------------------| 83 | |:LHLoad | ghl | load either timestamped backup or snapshot | 84 | |:LHLoadTimestamp | ght | load a backup with timestamp | 85 | |:LHLoadSnapshot | ghs | load a snapshot | 86 | |:LHWrite | ghw | write a backup with timestamp | 87 | |:LHWrite | gh Space | write a named backup (snapshot) | 88 | |:LHDiff | ghd | select a backup and open a split diff window | 89 | |:LHDelete | ghx | delete backups for current dir (all types) | 90 | |:LHBrowse | ghb | browse the current backup directory | 91 | 92 | The displayed mappings are only enabled if a prefix has been defined (here `gh` 93 | is used, as example). Commands use *fzf*, and you can select multiple files. 94 | 95 | 96 | 97 | 98 | #### Credits 99 | 100 | Bram Moolenaar for Vim 101 | 102 | 103 | 104 | 105 | #### License 106 | 107 | 108 | MIT 109 | 110 | 111 | -------------------------------------------------------------------------------- /doc/localhistory.txt: -------------------------------------------------------------------------------- 1 | *localhistory.txt* localhistory Version 0.2.0 Last change: 10 May 2020 2 | 3 | LOCAL HISTORY - TABLE OF CONTENTS *localhistory-toc* 4 | ============================================================================== 5 | 6 | Features |localhistory-features| 7 | Usage |localhistory-usage| 8 | Autobackup |localhistory-autobackup| 9 | Commands |localhistory-commands| 10 | Credits |localhistory-credits| 11 | License |localhistory-license| 12 | 13 | 14 | 15 | INTRODUCTION *localhistory-features* 16 | ============================================================================== 17 | 18 | This plugin creates automatic and/or manual backups for your files, in 19 | predefined paths. Backups are browsed with fzf or built-in |:Explore|. 20 | 21 | Features: 22 | 23 | * automatic backup directories generation, mirroring actual file path 24 | * save backups with timestamp or name (snapshots) 25 | * load/delete backups with fzf 26 | * perform diffs 27 | * optional autobackup on first load (configurable max size) 28 | * optional autobackup on save, if no recent backups are found (configurable) 29 | 30 | 31 | 32 | 33 | USAGE *localhistory-usage* 34 | ============================================================================== 35 | 36 | Default base directory is `~/.local_history` (Unix) or `~\local_history` 37 | (Windows): 38 | 39 | `let g:lh_basedir = '~/.local_history'` 40 | 41 | When saving a backup, the file will mirror the original path inside the base 42 | dir: 43 | 44 | `/path/to/file -> ~/.local_history/path/to/file` 45 | 46 | To enable mappings, specify a prefix, for example: 47 | > 48 | let g:lh_mappings_prefix = 'gh' 49 | 50 | Change the position in which files are opened (eg. |vs|, |sp|, |edit|, 51 | |botright| |vs|, etc): 52 | 53 | `let g:lh_open_mode = 'bo vs'` 54 | 55 | Set to 0 if you prefer horizontal split for diff windows: 56 | 57 | `let g:lh_vert_diff = 0` 58 | 59 | 60 | 61 | 62 | 63 | AUTOBACKUP *localhistory-autobackup* 64 | ============================================================================== 65 | 66 | To enable autobackup, you must first set these variables. It will only work 67 | after Vim has been restarted. Max size affects both types of autobackup. 68 | 69 | Activate autobackup on first access to a file, and max size (default 10240 70 | bytes): 71 | 72 | `let g:lh_autobackup_first = 1` 73 | `let g:lh_autobackup_size = 51200` 74 | 75 | Activate autobackup after save, if no recent backup is found (frequency in 76 | minutes): 77 | 78 | `let g:lh_autobackup_frequency = 60` 79 | 80 | 81 | 82 | COMMANDS *localhistory-commads* 83 | ============================================================================== 84 | 85 | 86 | ----------------------+-------------+----------------------------------------- 87 | Command | Map | Description ~ 88 | ----------------------+-------------+----------------------------------------- 89 | :LHLoad | ghl | load any backup 90 | :LHLoadTimestamp | ght | load a backup with timestamp 91 | :LHLoadSnapshot | ghs | load a snapshot 92 | :LHWrite | ghw | write a backup with timestamp 93 | :LHWrite | gh | write a named snapshot 94 | :LHDiff | ghd | select a backup and do a diff 95 | :LHDelete | ghx | delete backups for current directory 96 | :LHBrowse | ghb | browse the current backup directory 97 | 98 | The displayed mappings are only enabled if a prefix has been defined (here 99 | `gh` is used, as example). Commands use |fzf|, and you can select multiple 100 | files. 101 | 102 | 103 | 104 | 105 | 106 | CREDITS *localhistory-credits* 107 | ============================================================================== 108 | 109 | Bram Moolenaar for Vim 110 | 111 | 112 | 113 | LICENSE *localhistory-license* 114 | ============================================================================== 115 | 116 | MIT 117 | 118 | 119 | ============================================================================== 120 | vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: 121 | -------------------------------------------------------------------------------- /autoload/lh.vim: -------------------------------------------------------------------------------- 1 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 2 | " Write backup 3 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 4 | 5 | let s:sep = has('win32') ? '\' : '/' 6 | 7 | fun! lh#backup_file(name) abort 8 | " Write backup for current file. 9 | call s:make_backup_dir() 10 | 11 | if a:name != '' 12 | " named snapshot 13 | let stamp = "§ ".a:name 14 | else 15 | " use timestamp 16 | if has('win32') 17 | let stamp = printf("¦ %s_%s_%s %s", strftime("%Y"), strftime("%m"), 18 | \ strftime("%d"), strftime("%H_%M")) 19 | else 20 | let stamp = printf("¦ %s.%s.%s %s", strftime("%Y"), strftime("%m"), 21 | \ strftime("%d"), strftime("%H:%M")) 22 | endif 23 | endif 24 | 25 | let bkname = printf("%s%s%s %s", b:lh_dir, s:sep, expand("%:t"), stamp) 26 | call s:do_backup(bkname) 27 | endfun 28 | 29 | 30 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 31 | " Open/delete backups with fzf 32 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 33 | 34 | 35 | fun! lh#fzf(cmd, prompt) abort 36 | " Run the fzf selection dialogue. 37 | if !exists('g:loaded_fzf') 38 | silent! delcommand LHLoad 39 | silent! delcommand LHLoadSnapshot 40 | silent! delcommand LHLoadTimestamp 41 | silent! delcommand LHDelete 42 | silent! delcommand LHDiff 43 | return s:msg('without fzf installed, only :LHBrowse is allowed.') 44 | endif 45 | 46 | let s:fzf_dir = s:bkdir() 47 | 48 | let opts = {} 49 | let opts.source = a:cmd == 'timestamped' ? lh#find_files('timestamped') 50 | \ : a:cmd == 'snapshots' ? lh#find_files('snapshots') 51 | \ : lh#find_files('all') 52 | 53 | let opts.dir = s:fzf_dir 54 | let opts.down = '30%' 55 | let opts.options = [ '--prompt', a:prompt . ' >>> ' ] 56 | 57 | if executable('cat') 58 | let opts.options += ['--preview', 'cat {}'] 59 | endif 60 | 61 | if a:cmd != 'diff' 62 | let opts['sink*'] = a:cmd == 'delete' ? function('lh#delete_backup') 63 | \ : function('lh#open_backup') 64 | let opts.options += ['--multi'] 65 | else 66 | let opts.sink = function('lh#diff') 67 | endif 68 | 69 | if empty(opts.source) 70 | if a:cmd == 'timestamped' 71 | return s:msg('No timestamped backups for this file.') 72 | elseif a:cmd == 'snapshots' 73 | return s:msg('No snapshots for this file.') 74 | else 75 | return s:msg('No backups for this file.') 76 | endif 77 | endif 78 | 79 | call fzf#run(fzf#wrap(opts, 0)) 80 | endfun 81 | 82 | 83 | fun! lh#find_files(type) abort 84 | " Generate a list of files for fzf. 85 | if !s:valid_file() | return [] | endif 86 | 87 | let files = s:get_files() 88 | 89 | if a:type == 'snapshots' 90 | call filter(files, 'v:val =~ " § "') 91 | 92 | elseif a:type == 'timestamped' 93 | call filter(files, 'v:val !~ " § "') 94 | endif 95 | 96 | return map(files, 'fnamemodify(v:val, ":t")') 97 | endfun 98 | 99 | 100 | fun! lh#open_backup(files) abort 101 | " Open the selected files. 102 | for file in a:files 103 | exe g:lh_open_mode s:bkpath(file) 104 | let &ft = s:filetype 105 | endfor 106 | unlet s:fzf_dir 107 | endfun 108 | 109 | 110 | fun! lh#delete_backup(files) abort 111 | " Delete the selected files. 112 | let deleted_files = [] 113 | for file in a:files 114 | let path = s:fzf_dir . '/' . file 115 | if !delete(path) 116 | call add(deleted_files, "Deleted " . path) 117 | else 118 | call add(deleted_files, "Error deleting " . path) 119 | endif 120 | endfor 121 | botright 10new +setlocal\ bt=nofile\ bh=wipe\ noswf\ nobl 122 | 0put =deleted_files 123 | $put =' Press ENTER to dismiss' 124 | nnoremap :bw! 125 | syn keyword LHDeleted1 Deleted 126 | syn match LHDeleted2 '^Error deleting' 127 | syn match LHDeleted3 'Press ENTER to dismiss' 128 | hi def link LHDeleted1 Function 129 | hi def link LHDeleted2 Error 130 | hi def link LHDeleted3 WarningMsg 131 | unlet s:fzf_dir 132 | endfun 133 | 134 | 135 | fun! lh#diff(file) abort 136 | " Run a diff with the selected file. 137 | exe ( tabpagenr() - 1 ) . 'tabedit' @% 138 | diffthis 139 | let type = g:lh_vert_diff ? 'vertical' : '' 140 | exe type "diffsplit" s:bkpath(a:file) 141 | let &ft = s:filetype 142 | unlet s:fzf_dir 143 | endfun 144 | 145 | 146 | 147 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 148 | " Open backup directory in file browser 149 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 150 | 151 | 152 | fun! lh#browse() abort 153 | " Open backup directory in file browser. 154 | if !isdirectory(s:bkdir()) 155 | return s:msg('No backups for this file.') 156 | endif 157 | let cmd = get(g:, 'lh_browse_cmd', 'Explore') 158 | if !exists(':'.cmd) 159 | return s:msg('File browser is needed, read documentation.') 160 | endif 161 | exe cmd s:bkdir() 162 | endfun 163 | 164 | 165 | 166 | 167 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 168 | " From autocommands 169 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 170 | 171 | 172 | fun! lh#bufenter() abort 173 | " Perform operations on BufEnter autocommand. 174 | if !s:valid_file() | return | endif 175 | let b:lh_dir = s:bkdir() 176 | 177 | if g:lh_autobackup_first 178 | let file = resolve(expand("%:p")) 179 | if !filereadable(file) | return | endif 180 | if getfsize(file) > g:lh_autobackup_size | return | endif 181 | 182 | call s:make_backup_dir() 183 | let bkname = expand("%:t") . " § AUTOSAVE" 184 | if !filereadable(b:lh_dir . s:sep . bkname) 185 | call lh#backup_file('AUTOSAVE') 186 | endif 187 | endif 188 | endfun 189 | 190 | 191 | fun! lh#auto_backup() abort 192 | " Perform an automatic backup if option is set. 193 | if !s:valid_file() || !executable('stat') | return | endif 194 | if getfsize(resolve(expand("%:p"))) > g:lh_autobackup_size | return | endif 195 | 196 | let now = system('date +%s') 197 | let recent = 0 198 | let files = s:get_files() 199 | let basename = expand("%:t") 200 | 201 | " exclude other files in the same dir that are not the current one 202 | let file_pattern = '^' . s:bkpath(basename) . ' ¦\|§' 203 | call filter(files, 'v:val =~ file_pattern') 204 | 205 | " find the last time of modification for matching backup files 206 | for f in files 207 | let last_mod = system('stat -c %Y '.fnameescape(f)) 208 | if last_mod > recent 209 | let recent = last_mod 210 | endif 211 | endfor 212 | 213 | " create new backup if the most recent is older than lh_autobackup_frequency 214 | if now - recent > g:lh_autobackup_frequency*60 215 | call lh#backup_file('') 216 | endif 217 | endfun 218 | 219 | 220 | 221 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 222 | " Helpers 223 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 224 | 225 | 226 | fun! s:bkpath(file) abort 227 | " Escaped file path of the backup. 228 | if exists('s:fzf_dir') | return fnameescape(s:fzf_dir . '/' . a:file) 229 | elseif exists('b:lh_dir') | return fnameescape(b:lh_dir . '/' . a:file) 230 | else | return s:msg('invalid file', 1) 231 | endif 232 | endfun 233 | 234 | 235 | fun! s:bkdir() abort 236 | " The path for the backup directory for the current file. 237 | if has('win32') 238 | let base = fnamemodify(expand(g:lh_basedir), ":p:h") 239 | let file = fnamemodify(resolve(expand("%")), ":p:h") 240 | let path = base . '/' . substitute(file, ':', '', '') 241 | return expand(path) 242 | else 243 | return expand( 244 | \ fnamemodify(expand(g:lh_basedir), ":p:h") . 245 | \ fnamemodify(resolve(expand("%")), ":p:h")) 246 | endif 247 | endfun 248 | 249 | 250 | fun! s:make_backup_dir() 251 | " Ensure the backup directory exists. 252 | if !isdirectory(expand(g:lh_basedir)) 253 | call mkdir(expand(g:lh_basedir), "p") 254 | endif 255 | let b:lh_dir = s:bkdir() 256 | if !isdirectory(b:lh_dir) 257 | call mkdir(b:lh_dir, "p") 258 | endif 259 | endfun 260 | 261 | 262 | fun! s:get_files() abort 263 | " Return a list of valid files in the backup directory. 264 | let s:filetype = &filetype 265 | let b:lh_dir = s:bkdir() 266 | let files = split(globpath(b:lh_dir, "*", 1), '\n') 267 | let hidden = split(globpath(b:lh_dir, ".*", 1), '\n') 268 | return filter(extend(files, hidden), '!isdirectory(v:val)') 269 | endfun 270 | 271 | 272 | fun! s:valid_file() abort 273 | " If a file is eligible for backup or not. 274 | return buflisted(bufnr('')) && 275 | \ &buftype == '' && 276 | \ &filetype !~ 'git' && 277 | \ expand('%:p') !~ '\V\^'.expand(g:lh_basedir) 278 | endfun 279 | 280 | 281 | fun! s:do_backup(bkname) abort 282 | " Perform the actual backup operation on the file. 283 | if !filereadable(resolve(expand("%:p"))) 284 | return s:msg("source file doesn't exist", 1) 285 | endif 286 | let cmd = has('win32') ? 'copy' : 'cp' 287 | silent exe '!'.cmd shellescape(resolve(expand("%:p"))) shellescape(a:bkname) 288 | redraw! 289 | echom "Created" a:bkname 290 | endfun 291 | 292 | 293 | fun! s:msg(txt, ...) 294 | " Print a message, optionally as an error. 295 | if a:0 296 | echoerr '[local-history]' a:txt 297 | else 298 | echo '[local-history]' a:txt 299 | endif 300 | endfun 301 | 302 | 303 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 304 | 305 | " vim: et sw=4 ts=4 sts=4 fdm=indent fdn=1 306 | --------------------------------------------------------------------------------