├── doc └── sleuth.txt └── plugin └── sleuth.vim /doc/sleuth.txt: -------------------------------------------------------------------------------- 1 | *sleuth.txt* Heuristically set buffer options 2 | 3 | Author: Tim Pope 4 | Repo: https://github.com/tpope/vim-sleuth 5 | License: Same terms as Vim itself (see |license|) 6 | 7 | This plugin is only available if 'compatible' is not set. 8 | 9 | SUMMARY *sleuth* 10 | 11 | Automatic detection happens on the |BufNewFile|, |BufReadPost|, and 12 | |BufFilePost| events. Sleuth first consults modelines, then EditorConfig 13 | (https://editorconfig.org) files. If neither of these produces a value for 14 | 'expandtab' and 'shiftwidth', a heuristic algorithm is used to guess them, 15 | first on the current file, and then on other files with the same extension in 16 | the same and parent directories. 17 | 18 | The full list of detected options is as follows: 19 | 20 | Vim Option Safe? EditorConfig key ~ 21 | 'filetype' Y vim_filetype 22 | 'expandtab' Y indent_style 23 | 'shiftwidth' Y indent_size 24 | 'tabstop' Y tab_width 25 | 'textwidth' Y max_line_length 26 | 'fixendofline' Y insert_final_newline 27 | 'endofline' insert_final_newline 28 | 'fileformat' end_of_line 29 | 'fileencoding' charset 30 | 'bomb' charset 31 | 32 | Options marked "safe" are those guaranteed not to toggle 'modified'. These 33 | are the only options that will be set in 'nomodifiable' buffers, and the only 34 | options that Sleuth will detect in a modeline. Safe options will also be 35 | reapplied on every |FileType| event, so that they can override any |ftplugin| 36 | settings. 37 | 38 | Sleuth always sets 'softtabstop' to -1, effectively mirroring 'shiftwidth'. 39 | 40 | INTERFACE *sleuth-interface* 41 | 42 | *:Sleuth* 43 | :Sleuth Manually detect indentation. 44 | 45 | :verbose Sleuth Manually detect indentation, and display the reason 46 | each option was set. 47 | 48 | *SleuthIndicator()* 49 | SleuthIndicator() Indicator for inclusion in 'statusline'. Or use 50 | flagship.vim to have it included automatically. 51 | 52 | CONFIGURATION *sleuth-configuration* 53 | 54 | To override detection and force options for a particular file or directory, 55 | use a |modeline| or EditorConfig (https://editorconfig.org/), respectively. 56 | 57 | *g:sleuth_{filetype}_heuristics* 58 | To disable the heuristic algorithm for a given 'filetype', assign a false 59 | value to a variable named like g:sleuth_{filetype}_heuristics: 60 | > 61 | let g:sleuth_gitcommit_heuristics = 0 62 | < 63 | *g:sleuth_heuristics* 64 | Alternatively, one can disable heuristics by default, and opt-in for 65 | individual file types: 66 | > 67 | let g:sleuth_heuristics = 0 68 | let g:sleuth_perl_heuristics = 1 69 | < 70 | *g:sleuth_automatic* 71 | Sleuth used to support setting g:sleuth_automatic to a false value and instead 72 | calling :Sleuth selectively from |FileType| autocommands. This will no longer 73 | work quite right, and thus g:sleuth_automatic has been deprecated in favor of 74 | the heuristics options above. 75 | 76 | *g:sleuth_no_filetype_indent_on* 77 | Sleuth forces |:filetype-indent-on| by default, which enables file-type 78 | specific indenting algorithms and is highly recommended. To opt out: 79 | > 80 | let g:sleuth_no_filetype_indent_on = 1 81 | < 82 | vim:tw=78:et:ft=help:norl: 83 | -------------------------------------------------------------------------------- /plugin/sleuth.vim: -------------------------------------------------------------------------------- 1 | " sleuth.vim - Heuristically set buffer options 2 | " Maintainer: Tim Pope 3 | " Version: 2.0 4 | " GetLatestVimScripts: 4375 1 :AutoInstall: sleuth.vim 5 | 6 | if exists("#polyglot-sleuth") 7 | autocmd! polyglot-sleuth 8 | augroup! polyglot-sleuth 9 | unlet! g:loaded_sleuth 10 | let s:polyglot = 1 11 | endif 12 | 13 | if exists("g:loaded_sleuth") || v:version < 700 || &cp 14 | finish 15 | endif 16 | let g:loaded_sleuth = 1 17 | lockvar g:loaded_sleuth 18 | 19 | function! s:Warn(msg, ...) abort 20 | if !get(a:000, 0, 0) 21 | echohl WarningMsg 22 | echo a:msg 23 | echohl NONE 24 | endif 25 | return '' 26 | endfunction 27 | 28 | if exists('+shellslash') 29 | function! s:Slash(path) abort 30 | return tr(a:path, '\', '/') 31 | endfunction 32 | else 33 | function! s:Slash(path) abort 34 | return a:path 35 | endfunction 36 | endif 37 | 38 | function! s:Guess(source, detected, lines) abort 39 | let has_heredocs = a:detected.filetype =~# '^\%(perl\|php\|ruby\|[cz]\=sh\|bash\)$' 40 | let options = {} 41 | let heuristics = {'spaces': 0, 'hard': 0, 'soft': 0, 'checked': 0, 'indents': {}} 42 | let tabstop = get(a:detected.options, 'tabstop', get(a:detected.defaults, 'tabstop', [8]))[0] 43 | let softtab = repeat(' ', tabstop) 44 | let waiting_on = '' 45 | let prev_indent = -1 46 | let prev_line = '' 47 | 48 | for line in a:lines 49 | if len(waiting_on) 50 | if line =~# waiting_on 51 | let waiting_on = '' 52 | let prev_indent = -1 53 | let prev_line = '' 54 | endif 55 | continue 56 | elseif line =~# '^\s*$' 57 | continue 58 | elseif a:detected.filetype ==# 'python' && prev_line[-1:-1] =~# '[[\({]' 59 | let prev_indent = -1 60 | let prev_line = '' 61 | continue 62 | elseif line =~# '^=\w' && line !~# '^=\%(end\|cut\)\>' 63 | let waiting_on = '^=\%(end\|cut\)\>' 64 | elseif line =~# '^@@\+ -\d\+,\d\+ ' 65 | let waiting_on = '^$' 66 | elseif line !~# '[/<"`]' 67 | " No need to do other checks 68 | elseif line =~# '^\s*/\*' && line !~# '\*/' 69 | let waiting_on = '\*/' 70 | elseif line =~# '^\s*<\!--' && line !~# '-->' 71 | let waiting_on = '-->' 72 | elseif line =~# '^[^"]*"""' 73 | let waiting_on = '^[^"]*"""' 74 | elseif a:detected.filetype ==# 'go' && line =~# '^[^`]*`[^`]*$' 75 | let waiting_on = '^[^`]*`[^`]*$' 76 | elseif has_heredocs 77 | let waiting_on = matchstr(line, '<<\s*\([''"]\=\)\zs\w\+\ze\1[^''"`<>]*$') 78 | if len(waiting_on) 79 | let waiting_on = '^' . waiting_on . '$' 80 | endif 81 | endif 82 | 83 | let indent = len(matchstr(substitute(line, '\t', softtab, 'g'), '^ *')) 84 | if line =~# '^\t' 85 | let heuristics.hard += 1 86 | elseif line =~# '^' . softtab 87 | let heuristics.soft += 1 88 | endif 89 | if line =~# '^ ' 90 | let heuristics.spaces += 1 91 | endif 92 | let increment = prev_indent < 0 ? 0 : indent - prev_indent 93 | let prev_indent = indent 94 | let prev_line = line 95 | if increment > 1 && (increment < 4 || increment % 4 == 0) 96 | if has_key(heuristics.indents, increment) 97 | let heuristics.indents[increment] += 1 98 | else 99 | let heuristics.indents[increment] = 1 100 | endif 101 | let heuristics.checked += 1 102 | endif 103 | if heuristics.checked >= 32 && (heuristics.hard > 3 || heuristics.soft > 3) && get(heuristics.indents, increment) * 2 > heuristics.checked 104 | if heuristics.spaces 105 | break 106 | elseif !exists('no_space_indent') 107 | let no_space_indent = stridx("\n" . join(a:lines, "\n"), "\n ") < 0 108 | if no_space_indent 109 | break 110 | endif 111 | endif 112 | break 113 | endif 114 | endfor 115 | 116 | let a:detected.heuristics[a:source] = heuristics 117 | 118 | let max_frequency = 0 119 | for [shiftwidth, frequency] in items(heuristics.indents) 120 | if frequency > max_frequency || frequency == max_frequency && +shiftwidth < get(options, 'shiftwidth') 121 | let options.shiftwidth = +shiftwidth 122 | let max_frequency = frequency 123 | endif 124 | endfor 125 | 126 | if heuristics.hard && !heuristics.spaces && 127 | \ !has_key(a:detected.options, 'tabstop') 128 | let options = {'expandtab': 0, 'shiftwidth': 0} 129 | elseif heuristics.hard > heuristics.soft 130 | let options.expandtab = 0 131 | let options.tabstop = tabstop 132 | else 133 | if heuristics.soft 134 | let options.expandtab = 1 135 | endif 136 | if heuristics.hard || has_key(a:detected.options, 'tabstop') || 137 | \ stridx(join(a:lines, "\n"), "\t") >= 0 138 | let options.tabstop = tabstop 139 | elseif !&g:shiftwidth && has_key(options, 'shiftwidth') && 140 | \ !has_key(a:detected.options, 'shiftwidth') 141 | let options.tabstop = options.shiftwidth 142 | let options.shiftwidth = 0 143 | endif 144 | endif 145 | 146 | call map(options, '[v:val, a:source]') 147 | call extend(a:detected.options, options, 'keep') 148 | endfunction 149 | 150 | function! s:Capture(cmd) abort 151 | redir => capture 152 | silent execute a:cmd 153 | redir END 154 | return capture 155 | endfunction 156 | 157 | let s:modeline_numbers = { 158 | \ 'shiftwidth': 'shiftwidth', 'sw': 'shiftwidth', 159 | \ 'tabstop': 'tabstop', 'ts': 'tabstop', 160 | \ 'textwidth': 'textwidth', 'tw': 'textwidth', 161 | \ } 162 | let s:modeline_booleans = { 163 | \ 'expandtab': 'expandtab', 'et': 'expandtab', 164 | \ 'fixendofline': 'fixendofline', 'fixeol': 'fixendofline', 165 | \ } 166 | function! s:ParseOptions(declarations, into, ...) abort 167 | for option in a:declarations 168 | if has_key(s:modeline_booleans, matchstr(option, '^\%(no\)\=\zs\w\+$')) 169 | let a:into[s:modeline_booleans[matchstr(option, '^\%(no\)\=\zs\w\+')]] = [option !~# '^no'] + a:000 170 | elseif has_key(s:modeline_numbers, matchstr(option, '^\w\+\ze=[1-9]\d*$')) 171 | let a:into[s:modeline_numbers[matchstr(option, '^\w\+')]] = [str2nr(matchstr(option, '\d\+$'))] + a:000 172 | elseif option =~# '^\%(ft\|filetype\)=[[:alnum:]._-]*$' 173 | let a:into.filetype = [matchstr(option, '=\zs.*')] + a:000 174 | endif 175 | if option ==# 'nomodeline' || option ==# 'noml' 176 | return 1 177 | endif 178 | endfor 179 | return 0 180 | endfunction 181 | 182 | function! s:ModelineOptions() abort 183 | let options = {} 184 | if !&l:modeline && (&g:modeline || s:Capture('setlocal') =~# '\\\@' && 185 | \ s:Capture('verbose setglobal modeline?') !=# s:Capture('verbose setlocal modeline?')) 186 | return options 187 | endif 188 | let modelines = get(b:, 'sleuth_modelines', get(g:, 'sleuth_modelines', 5)) 189 | if line('$') > 2 * modelines 190 | let lnums = range(1, modelines) + range(line('$') - modelines + 1, line('$')) 191 | else 192 | let lnums = range(1, line('$')) 193 | endif 194 | for lnum in lnums 195 | if s:ParseOptions(split(matchstr(getline(lnum), 196 | \ '\%(\S\@= 0 422 | let cmd .= ' ' . setting 423 | endif 424 | if !&verbose || a:silent 425 | if has_key(s:booleans, option) 426 | let msg .= ' ' . (value[0] ? '' : 'no') . get(s:short_options, option, option) 427 | else 428 | let msg .= ' ' . get(s:short_options, option, option) . '=' . value[0] 429 | endif 430 | continue 431 | endif 432 | if len(value) > 1 433 | if value[1] ==# a:detected.bufname 434 | let file = '%' 435 | else 436 | let file = value[1] =~# '/' ? fnamemodify(value[1], ':~:.') : value[1] 437 | if file !=# value[1] && file[0:0] !=# '~' 438 | let file = './' . file 439 | endif 440 | endif 441 | if len(value) > 2 442 | let file .= ' line ' . value[2] 443 | endif 444 | echo printf(':setlocal %-21s " from %s', setting, file) 445 | else 446 | echo ':setlocal ' . setting 447 | endif 448 | endfor 449 | if !&verbose && !empty(msg) && !a:silent 450 | echo ':setlocal' . msg 451 | endif 452 | if has_key(options, 'shiftwidth') 453 | let cmd .= ' softtabstop=' . (exists('*shiftwidth') ? -1 : options.shiftwidth[0]) 454 | else 455 | call s:Warn(':Sleuth failed to detect indent settings', a:silent) 456 | endif 457 | return cmd ==# 'setlocal' ? '' : cmd 458 | endfunction 459 | 460 | function! s:UserOptions(ft, name) abort 461 | if exists('b:sleuth_' . a:name) 462 | let source = 'b:sleuth_' . a:name 463 | elseif exists('g:sleuth_' . a:ft . '_' . a:name) 464 | let source = 'g:sleuth_' . a:ft . '_' . a:name 465 | endif 466 | if !exists('l:source') || type(eval(source)) == type(function('tr')) 467 | return {} 468 | endif 469 | let val = eval(source) 470 | let options = {} 471 | if type(val) == type('') 472 | call s:ParseOptions(split(substitute(val, '\S\@= 0') 485 | return options 486 | endfunction 487 | 488 | function! s:DetectDeclared() abort 489 | let detected = {'bufname': s:Slash(@%), 'declared': {}} 490 | let absolute_or_empty = detected.bufname =~# '^$\|^\a\+:\|^/' 491 | if &l:buftype =~# '^\%(nowrite\)\=$' && !absolute_or_empty 492 | let detected.bufname = substitute(s:Slash(getcwd()), '/\=$', '/', '') . detected.bufname 493 | let absolute_or_empty = 1 494 | endif 495 | let detected.path = absolute_or_empty ? detected.bufname : '' 496 | let pre = substitute(matchstr(detected.path, '^\a\a\+\ze:'), '^\a', '\u&', 'g') 497 | if len(pre) && exists('*' . pre . 'Real') 498 | let detected.path = s:Slash(call(pre . 'Real', [detected.path])) 499 | endif 500 | 501 | try 502 | if len(detected.path) && exists('*ExcludeBufferFromDiscovery') && !empty(ExcludeBufferFromDiscovery(detected.path, 'sleuth')) 503 | let detected.path = '' 504 | endif 505 | catch 506 | endtry 507 | let [detected.editorconfig, detected.root] = s:DetectEditorConfig(detected.path) 508 | call extend(detected.declared, s:EditorConfigToOptions(detected.editorconfig)) 509 | call extend(detected.declared, s:ModelineOptions()) 510 | return detected 511 | endfunction 512 | 513 | function! s:DetectHeuristics(into) abort 514 | let detected = a:into 515 | let filetype = split(&l:filetype, '\.', 1)[0] 516 | if get(detected, 'filetype', '*') ==# filetype 517 | return detected 518 | endif 519 | let detected.filetype = filetype 520 | let options = copy(detected.declared) 521 | let detected.options = options 522 | let detected.heuristics = {} 523 | if has_key(detected, 'patterns') 524 | call remove(detected, 'patterns') 525 | endif 526 | let detected.defaults = s:UserOptions(filetype, 'defaults') 527 | if empty(filetype) || !get(b:, 'sleuth_automatic', 1) || empty(get(b:, 'sleuth_heuristics', get(g:, 'sleuth_' . filetype . '_heuristics', get(g:, 'sleuth_heuristics', 1)))) 528 | return detected 529 | endif 530 | if s:Ready(detected) 531 | return detected 532 | endif 533 | 534 | let lines = getline(1, 1024) 535 | call s:Guess(detected.bufname, detected, lines) 536 | if s:Ready(detected) 537 | return detected 538 | elseif get(options, 'shiftwidth', [4])[0] < 4 && stridx(join(lines, "\n"), "\t") == -1 539 | let options.expandtab = [1, detected.bufname] 540 | return detected 541 | endif 542 | let dir = len(detected.path) ? fnamemodify(detected.path, ':h') : '' 543 | let root = len(detected.root) ? fnamemodify(detected.root, ':h') : dir ==# s:Slash(expand('~')) ? dir : fnamemodify(dir, ':h') 544 | if detected.bufname =~# '^\a\a\+:' || root ==# '.' || !isdirectory(root) 545 | let dir = '' 546 | endif 547 | let c = get(b:, 'sleuth_neighbor_limit', get(g:, 'sleuth_neighbor_limit', 8)) 548 | if c <= 0 || empty(dir) 549 | let detected.patterns = [] 550 | elseif type(get(b:, 'sleuth_globs')) == type([]) 551 | let detected.patterns = b:sleuth_globs 552 | elseif type(get(g:, 'sleuth_' . detected.filetype . '_globs')) == type([]) 553 | let detected.patterns = get(g:, 'sleuth_' . detected.filetype . '_globs') 554 | else 555 | let detected.patterns = ['*' . matchstr(detected.bufname, '/\@ 0 && dir !~# '^$\|^//[^/]*$' && dir !=# fnamemodify(dir, ':h') 565 | for pattern in detected.patterns 566 | for neighbor in split(glob(dir.'/'.pattern), "\n")[0:7] 567 | if neighbor !=# detected.path && filereadable(neighbor) 568 | call s:Guess(neighbor, detected, readfile(neighbor, '', 256)) 569 | let c -= 1 570 | endif 571 | if s:Ready(detected) 572 | return detected 573 | endif 574 | if c <= 0 575 | break 576 | endif 577 | endfor 578 | if c <= 0 579 | break 580 | endif 581 | endfor 582 | if len(dir) <= len(root) 583 | break 584 | endif 585 | let dir = fnamemodify(dir, ':h') 586 | endwhile 587 | if !has_key(options, 'shiftwidth') 588 | let detected.options = copy(detected.declared) 589 | endif 590 | return detected 591 | endfunction 592 | 593 | function! s:Init(redetect, unsafe, do_filetype, silent) abort 594 | if !a:redetect && exists('b:sleuth.defaults') 595 | let detected = b:sleuth 596 | endif 597 | unlet! b:sleuth 598 | if &l:buftype !~# '^\%(nowrite\|nofile\|acwrite\)\=$' 599 | return s:Warn(':Sleuth disabled for buftype=' . &l:buftype, a:silent) 600 | endif 601 | if &l:filetype ==# 'netrw' 602 | return s:Warn(':Sleuth disabled for filetype=' . &l:filetype, a:silent) 603 | endif 604 | if &l:binary 605 | return s:Warn(':Sleuth disabled for binary files', a:silent) 606 | endif 607 | if !exists('detected') 608 | let detected = s:DetectDeclared() 609 | endif 610 | let setfiletype = '' 611 | if a:do_filetype && has_key(detected.declared, 'filetype') 612 | let filetype = detected.declared.filetype[0] 613 | if filetype !=# &l:filetype || empty(filetype) 614 | let setfiletype = 'setlocal filetype=' . filetype 615 | else 616 | let setfiletype = 'setfiletype ' . filetype 617 | endif 618 | endif 619 | exe setfiletype 620 | call s:DetectHeuristics(detected) 621 | let cmd = s:Apply(detected, (a:do_filetype ? ['filetype'] : []) + (a:unsafe ? s:all_options : s:safe_options), a:silent) 622 | let b:sleuth = detected 623 | if exists('s:polyglot') && !a:silent 624 | call s:Warn('Charlatan :Sleuth implementation in vim-polyglot has been found and disabled.') 625 | call s:Warn('To get rid of this message, uninstall vim-polyglot, or disable the') 626 | call s:Warn('corresponding feature in your vimrc:') 627 | call s:Warn(' let g:polyglot_disabled = ["autoindent"]') 628 | endif 629 | return cmd 630 | endfunction 631 | 632 | function! s:AutoInit() abort 633 | return s:Init(1, 1, 1, 1) 634 | endfunction 635 | 636 | function! s:Sleuth(line1, line2, range, bang, mods, args) abort 637 | let safe = a:bang || expand("") =~# '\%(^\|\.\.\)FileType ' 638 | return s:Init(!a:bang, !safe, !safe, 0) 639 | endfunction 640 | 641 | if !exists('g:did_indent_on') && !get(g:, 'sleuth_no_filetype_indent_on') 642 | filetype indent on 643 | elseif !exists('g:did_load_filetypes') 644 | filetype on 645 | endif 646 | 647 | function! SleuthIndicator() abort 648 | let sw = &shiftwidth ? &shiftwidth : &tabstop 649 | if &expandtab 650 | let ind = 'sw='.sw 651 | elseif &tabstop == sw 652 | let ind = 'ts='.&tabstop 653 | else 654 | let ind = 'sw='.sw.',ts='.&tabstop 655 | endif 656 | if &textwidth 657 | let ind .= ',tw='.&textwidth 658 | endif 659 | if exists('&fixendofline') && !&fixendofline && !&endofline 660 | let ind .= ',noeol' 661 | endif 662 | return ind 663 | endfunction 664 | 665 | augroup sleuth 666 | autocmd! 667 | autocmd BufNewFile,BufReadPost * nested 668 | \ if get(g:, 'sleuth_automatic', 1) 669 | \ | exe s:AutoInit() | endif 670 | autocmd BufFilePost * nested 671 | \ if (@% !~# '^!' || exists('b:sleuth')) && get(g:, 'sleuth_automatic', 1) 672 | \ | exe s:AutoInit() | endif 673 | autocmd FileType * nested 674 | \ if exists('b:sleuth') | exe s:Init(0, 0, 0, 1) | endif 675 | autocmd User Flags call Hoist('buffer', 5, 'SleuthIndicator') 676 | augroup END 677 | 678 | command! -bar -bang Sleuth exe s:Sleuth(, , +"", 0, "", ) 679 | --------------------------------------------------------------------------------