diff --git a/vim_plugins/yaifa.vim b/vim_plugins/yaifa.vim new file mode 100644 index 000000000..d9bcf5447 --- /dev/null +++ b/vim_plugins/yaifa.vim @@ -0,0 +1,472 @@ +" YAIFA: Yet Another Indent Finder, Almost... +" Version: 1.3 +" Modified: 2010-08-17 +" Author: Israel Chauca F. +" +" This plug-in will try to detect the kind of indentation in your file and set +" Vim's options to keep it that way. It recognizes three types of indentation: +" +" 1.- Space: Only spaces are used to indent. +" +" 2.- Tab: Only tabs are used. +" +" 3.- Mixed: A combination of tabs and space is used. e.g.: a tab stands for 8 +" spaces, but each indentation level is 4 spaces. +" +" Use :YAIFAMagic to manually set the indenting settings for the current file. +" +" Depending on the system set-up and the file size scanning too many lines can +" be painfully slow, so YAIFA processes 2048 lines by default, that value can +" be changed with the following line in your .vimrc: +" +" let yaifa_max_lines = 4096 +" +" This script is a port to VimL from Philippe Fremy's Python script Indent +" Finder, hence the "Almost" part of the name. + +if exists('g:loaded_yaifa') + finish +endif + +let g:loaded_yaifa = 1 + +" Depending on your system and file size, scanning too many lines can be +" painfully slow. +if exists('g:yaifa_max_lines') + let s:max_lines = g:yaifa_max_lines +else + let s:max_lines = 1024*2 +endif + +redir => redir | silent verbose set sw? | redir END +let s:swset = len(split(redir,"\n")) > 1 + +if &expandtab + let s:default_indent = 'space' + let s:default_tab_width = s:swset ? &sw : 2 +else + let s:default_indent = 'tab' + let s:default_tab_width = s:swset ? &sw : 4 +endif + + +let s:verbose_quiet = 0 +let s:verbose_info = 1 +let s:verbose_debug = 2 +let s:verbose_deep = 3 +if exists('g:yaifa_verbosity') + let s:verbosity = g:yaifa_verbosity +else + let s:default_verbosity = s:verbose_quiet + let s:verbosity = s:default_verbosity +endif + +let s:default_result = [s:default_indent, s:default_tab_width] +let s:nb_processed_lines = 0 + +let s:NoIndent = "NoIndent" +let s:SpaceOnly = "SpaceOnly" +let s:TabOnly = "TabOnly" +let s:Mixed = "Mixed" +let s:BeginSpace = "BeginSpace" +let s:indent_re = '\m^\(\s\+\)\(\S.\+\)$' +let s:mixed_re = '\m^\(\t\)\+\( \+\)$' + +function! s:log(level, s) + if a:level <= s:verbosity + echomsg s:nb_processed_lines . ':' . a:s + endif +endfunction + +function! s:info(s) + call s:log(s:verbose_info, 'info:' . a:s) +endfunction + +function! s:dbg(s) + call s:log(s:verbose_debug, 'dbg:' . a:s) +endfunction + +function! s:deepdbg(s) + call s:log(s:verbose_deep, 'deep:' . a:s) +endfunction + +function! s:clear() + let s:lines = {} + for i in range(2,9) + let s:lines[i] = 0 + let s:lines[-1 * i ] = 0 + endfor + let s:lines.tab = 0 + + let s:nb_processed_lines = 0 + let s:nb_indent_hint = 0 + let s:skip_next_line = 0 + let s:previous_line_info = [] +endfunction + +function! s:parse_file() + "let nb_lines = line('$') < s:max_lines ? line('$') : s:max_lines + let nb_lines = line('$') + let i = 1 + "while i <= nb_lines + while i <= nb_lines && s:nb_processed_lines < s:max_lines + call s:analyse_line(getline(i)) + let i += 1 + endw +endf + +function! s:analyse_line(line) + let line = substitute(a:line, '\m\n', '', '') + call s:deepdbg('analyse_line: ' . substitute(substitute(line, '\m\t', '\\t', 'g'), '\m ', '·','g')) + let s:nb_processed_lines += 1 + let skip_current_line = s:skip_next_line + let s:skip_next_line = 0 + if line =~ '\m\\$' + call s:deepdbg('analyse_line: Ignoring next line!') + let s:skip_next_line = 1 + endif + + if skip_current_line + call s:deepdbg('analyse_line: Ignoring next line!') + return + endif + let ret = s:analyse_line_indentation(line) + if ret + let s:nb_indent_hint += 1 + call s:deepdbg('analyse_line: Result of line analysis: ' . ret) + endif + return ret +endfunction + +function! s:analyse_line_type(line) + let mixed_mode = 0 + let tab_part = "" + let space_part = "" + + if a:line !~ '\m^$' && a:line !~ '\m^\s' + cal s:deepdbg('analyse_line_type: line is not empty and not indented: ') + return [s:NoIndent, ''] + endif + + if a:line !~ s:indent_re + cal s:deepdbg('analyse_line_type: line is not indented') + return [] + else + let indent_part = substitute(a:line, s:indent_re, '\1', '') + let text_part = substitute(a:line, s:indent_re, '\2', '') + endif + call s:deepdbg('analyse_line_type: indent_part="' . substitute(substitute(substitute(indent_part, '\m\n', '\\n', 'g'), '\m\t', '\\t', 'g'), ' ', '·', 'g') . '"') + + if text_part =~ '\m^\*' + " continuation of a C/C++ comment, unlikely to be indented correctly + return [] + endif + + if a:line =~ '\m^(/\*\|#)' + " python, C/C++ comment, might not be indented correctly + return [] + endif + + if indent_part =~ '\m\t' && indent_part =~ '\m ' + "Mixed mode + "let mo = indent_part =~ s:mixed_re + if indent_part !~ s:mixed_re + " Line is not composed of "\t\t\t " + return [] + endif + let mixed_mode = 1 + let tab_part = substitute(indent_part, s:mixed_re, '\1','') + let space_part = substitute(indent_part, s:mixed_re, '\2','') + endif + + if mixed_mode + if len(space_part) >= 8 + "this is not mixed mode, this is garbage ! + return [] + endif + return [s:Mixed, tab_part, space_part] + endif" + + if indent_part =~ '\m\t' + return [s:TabOnly, indent_part] + endif + + if indent_part =~ '\m ' + if len(indent_part) < 8 + " this could be mixed mode too + return [s:BeginSpace, indent_part] + else + " this is really a line indented with spaces + return [s:SpaceOnly, indent_part] + endif + endif + echoerr 'We should never get here!' +endfunction + +function! s:analyse_line_indentation(line) + let previous_line_info = s:previous_line_info + let current_line_info = s:analyse_line_type(a:line) + let s:previous_line_info = current_line_info + + if len(current_line_info) == 0 || len(previous_line_info) == 0 + "call s:deepdbg('analyse_line_indentation: Not enough info to analyse line : ' . string(previous_line_info) . ':' . string(current_line_info)) + return 0 + endif + + let t = [previous_line_info[0], current_line_info[0]] + call s:deepdbg('analyse_line_indentation: Indent analysis: ' . string(t)) + + if t == [s:TabOnly, s:TabOnly] + \ || t == [s:NoIndent, s:TabOnly] + if len(current_line_info[1]) - len(previous_line_info[1]) == 1 + let s:lines['tab'] += 1 + return 'tab' + endif + elseif t == [s:SpaceOnly, s:SpaceOnly] + \ || t == [s:BeginSpace, s:SpaceOnly] + \ || t == [s:NoIndent, s:SpaceOnly] + let nb_space = len(current_line_info[1]) - len(previous_line_info[1]) + if 1 < nb_space && nb_space <= 8 + "execute 'let key = "space' . nb_space . '"' + let key = nb_space + let s:lines[key] += 1 + return key + endif + elseif t == [s:BeginSpace, s:BeginSpace] + \ || t == [s:NoIndent, s:BeginSpace] + let nb_space = len(current_line_info[1]) - len(previous_line_info[1]) + if 1 < nb_space && nb_space <= 8 + "execute 'let key1 = "space' . nb_space . '"' + "execute 'let key2 = "mixed' . nb_space . '"' + let key1 = nb_space + let key2 = -1 * nb_space + let s:lines[key1] += 1 + let s:lines[key2] += 1 + return key1 + endif + elseif t == [s:BeginSpace, s:TabOnly] + " We assume that mixed indentation used 8 chars tabs + if len(current_line_info[1]) == 1 + let nb_space = len(current_line_info[1]) - len(previous_line_info[1]) + if 1 < nb_space && nb_space <= 8 + "execute 'let key = "mixed' . nb_space . '"' + let key = -1 * nb_space + let s:lines[key] += 1 + return key + endif + endif + elseif t == [s:TabOnly, s:Mixed] + let tab_part = current_line_info[1] + let space_part = current_line_info[2] + if len(previous_line_info[1]) == len(tab_part) + let nb_space = len(space_part) + if 1 < nb_space && nb_space <= 8 + "execute 'let key = "mixed' . nb_space . '"' + let key = -1 * nb_space + let s:lines[key] += 1 + return key + endif + endif + elseif t == [s:Mixed, s:TabOnly] + let tab_part = previous_line_info[1] + let space_part = previous_line_info[2] + if len(tab_part) + 1 == len(current_line_info[1]) + let nb_space = 8 - len(space_part) + if 1 < nb_space && nb_space <= 8 + "execute 'let key = "mixed' . nb_space . '"' + let key = -1 * nb_space + let s:lines[key] += 1 + return key + endif + endif + endif + return 0 +endfunction + +function! s:results() + call s:dbg( "Nb of scanned lines : " . s:nb_processed_lines) + call s:dbg( "Nb of indent hint : " . s:nb_indent_hint) + "call s:dbg( "Collected data:") + for key in keys(s:lines) + if s:lines[key] > 0 + call s:dbg( ' Key ' . key . ' => ' . s:lines[key]) + endif + endfor + let spaces = [] + let mixed = [] + for i in range(2,9) + "execute 'let spaces = add(spaces, s:lines.space' . i . ')' + let spaces = add(spaces, s:lines[i]) + "execute 'let mixed = add(mixed, s:lines.mixed' . i . ')' + let mixed = add(mixed, s:lines[-1 * i]) + endfor + let max_line_space = max(spaces) + let max_line_mixed = max(mixed) + let max_line_tab = s:lines.tab + + call s:dbg( 'max_line_space: ' . max_line_space ) + call s:dbg( 'max_line_mixed: ' . max_line_mixed ) + call s:dbg( 'max_line_tab: ' . max_line_tab ) + + + """ Result analysis + " + " 1. Space indented file + " - lines indented with less than 8 space will fill mixed and space array + " - lines indented with 8 space or more will fill only the space array + " - almost no lines indented with tab + " + " => more lines with space than lines with mixed + " => more a lot more lines with space than tab + " + " 2. Tab indented file + " - most lines will be tab only + " - very few lines as mixed + " - very few lines as space only + " + " => a lot more lines with tab than lines with mixed + " => a lot more lines with tab than lines with space + " + " 3. Mixed tab/space indented file + " - some lines are tab-only (lines with exactly 8 step indentation) + " - some lines are space only (less than 8 space) + " - all other lines are mixed + " + " If mixed is tab + 2 space indentation: + " - a lot more lines with mixed than with tab + " If mixed is tab + 4 space indentation + " - as many lines with mixed than with tab + " + " If no lines exceed 8 space, there will be only lines with space + " and tab but no lines with mixed. Impossible to detect mixed indentation + " in this case, the file looks like it's actually indented as space only + " and will be detected so. + " + " => same or more lines with mixed than lines with tab only + " => same or more lines with mixed than lines with space only + " + + let result = [] + " Detect space indented file + if max_line_space >= max_line_mixed && max_line_space > max_line_tab + let nb = 0 + let indent_value = 0 + for i in range(8,2,-1) + "execute 'let m = s:lines.space' . i . ' > floor(nb * 1.1)' + "if s:lines[i] > floor(nb * 1.1) + if s:lines[i] * 10 > nb * 11 + let indent_value = i + "execute 'let nb = s:lines.space' . i + let nb = s:lines[i] + endif + endfor + + if indent_value == 0 + let result = s:default_result + else + let result = ['space', indent_value] + endif + + " Detect tab files + elseif max_line_tab > max_line_mixed && max_line_tab > max_line_space + let result = ['tab', s:default_tab_width] + + " Detect mixed files + elseif max_line_mixed >= max_line_tab && max_line_mixed > max_line_space + let nb = 0 + let indent_value = 0 + for i in range(-8,-2) + "execute 'let m = s:lines.mixed' . i . ' > floor(nb * 1.1)' + "let m = s:lines[i] > floor(nb * 1.1) + "if s:lines[i] > floor(nb * 1.1) + if s:lines[i] * 10 > nb * 11 + let indent_value = -1 * i + "execute 'let nb = s:lines.mixed' . i + let nb = s:lines[i] + endif + endfor + + if indent_value == 0 + let result = s:default_result + else + let result = ['mixed', [8, indent_value]] + endif + + " Not enough information to make a decision + else + let result = s:default_result + endif + call s:info('Result: ' . string(result)) + return result +endfunction + +function! YAIFA(...) + + " The magic starts here + call s:clear() + call s:parse_file() + + let result = s:results() + + if result[0] == 'space' + call s:info('space') + "spaces: + " => set sts to the number of spaces + " => set tabstop to 8 + " => expand tabs to spaces + " => set shiftwidth to the number of spaces + let cmd = 'set sts=' . result[1] . ' | set tabstop=8 | set expandtab | set shiftwidth=' . result[1] + elseif result[0] == 'tab' + call s:info('tab') + "tab: + " => set sts to 0 + " => set tabstop to preferred value + " => set expandtab to false + " => set shiftwidth to tabstop + let cmd = 'set sts=0 | set tabstop=' . s:default_tab_width . ' | set noexpandtab | set shiftwidth=' . s:default_tab_width + elseif result[0] == 'mixed' + call s:info('mixed') + "tab: + " => set sts to 0 + " => set tabstop to tab_indent + " => set expandtab to false + " => set shiftwidth to space_indent + let s:ts = result[1][0] + let s:sw = result[1][1] + "echom "s:sw: " . s:sw + "if s:sw == "" && (s:ts - (2*(s:ts/2))) == 0 " l mod 2 + if s:sw == "" && s:ts > 2 + let s:sw = s:ts/2 + elseif s:sw == "" + let s:sw = s:ts + endif + + let cmd = 'set sts=' . s:sw . ' | set tabstop=' . s:ts . ' | set noexpandtab | set shiftwidth=' . s:sw + endif + execute cmd + let b:yaifa_set = 1 + if a:0 > 0 + if a:1 == 2 + if result[0] == "space" + return "space" . result[1] + elseif result[0] == "tab" + return "tab" + else + return "mixed" . s:sw + endif + endif + else + echom cmd + endif +endfunction + +function! YAIFAGetVar(var) + exec "return s:".a:var +endfunction + +augroup YAIFA + au! YAIFA + au BufRead * call YAIFA(1) +augroup End + +command -nargs=0 -bar YAIFAMagic call YAIFA()