├── 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 |
--------------------------------------------------------------------------------