" " Copyright 2006 Tye Zdrojewski " " Licensed under the Apache License, Version 2.0 (the "License"); you may not " use this file except in compliance with the License. You may obtain a copy of " the License at " " http://www.apache.org/licenses/LICENSE-2.0 " " Unless required by applicable law or agreed to in writing, software distributed " under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR " CONDITIONS OF ANY KIND, either express or implied. See the License for the " specific language governing permissions and limitations under the License. " " " Plugin: " " Indent Anything " " Version: 1.2 " " Description: " " This is an indentation script that calculates the indent level based " on begin/end syntax pairs and line-continuation patterns. It allows one " to create an indent script without writing any code, taking it's " instruction from configurable values. " " Included with this script is Javascript indentation, an example that " explains the configurable values. " " " Installation: " " Place this file in your home directory under ~/.vim/indent/, or replace " the system indent/javascript.vim file to affect all users. " " Maintainer: Tye Z. < z d r o @ y a h o o . c o m > " " Customization: " " The only thing that can really be customized at this point is whether or " not a line is echoed explaining the indentation result. To turn this on, " set the following variable like so: " " let b:indent_anything_echo = 1 " " " History: " " 1.2 - made some functions script-local to prevent naming collisions " - fixed some broken indentation in the middle of a block comment, " which showed up in Javascript indentation. " let s:supportedVimVersion = 700 if version < s:supportedVimVersion echoerr "IndentAnything only supported for Vim " . s:supportedVimVersion . " and up." finish endif " " Initialize everything needed by this script. Only set those values that are " not set already. " function! s:IndentAnythingInit() let b:IndentAnythingInitialized = 1 " Start with a regular expression that will never match. Matching " will influence behavior, which the defaults should not do. let s:nonMatcher = '[x]\&[^x]' if !exists('b:commentRE') let b:commentRE = s:nonMatcher endif if !exists('b:lineCommentRE') let b:lineCommentRE = s:nonMatcher endif if !exists('b:blockCommentRE') let b:blockCommentRE = s:nonMatcher endif if !exists('b:stringRE') let b:stringRE = s:nonMatcher endif if !exists('b:singleQuoteStringRE') let b:singleQuoteStringRE = s:nonMatcher endif if !exists('b:doubleQuoteStringRE') let b:doubleQuoteStringRE = s:nonMatcher endif if !exists('b:blockCommentStartRE') let b:blockCommentStartRE = s:nonMatcher endif if !exists('b:blockCommentMiddleRE') let b:blockCommentMiddleRE = s:nonMatcher endif if !exists('b:blockCommentEndRE') let b:blockCommentEndRE = s:nonMatcher endif if !exists('b:blockCommentMiddleExtra') let b:blockCommentMiddleExtra = 0 endif if !exists('b:indentTrios') let b:indentTrios = [] endif if !exists('b:lineContList') let b:lineContList = [] endif if !exists('b:contTraversesLineComments') let b:contTraversesLineComments = 1 endif if !exists('b:indent_anything_echo') let b:indent_anything_echo = 0 endif endfunction function! s:SynHere() return synIDattr(synID(line('.'), col('.'), 1), "name") endfunction " " Returns true if the cursor is currently inside a comment or a string " function! InCommentOrString() let syn = synIDattr(synID(line("."), col("."), 1), "name") if syn =~ b:commentRE || syn =~ b:stringRE return 1 endif return 0 endfunction " " Returns true if the given line is a comment line (b:lineCommentRE) " function! s:IsLineComment(linenum) let cursor = getpos('.') exec a:linenum normal ^ let l:iscomment = 0 let l:syn = synIDattr(synID(line('.'), col('.'), 1), "name") if l:syn =~ b:lineCommentRE " b:commentRE || l:syn =~ b:stringRE let l:iscomment = 1 endif call setpos('.', cursor) return l:iscomment endfunction " " Returns true if the given line is a comment line (b:lineCommentRE) " function! s:IsComment(linenum) let cursor = getpos('.') exec a:linenum normal ^ let l:iscomment = 0 let l:syn = synIDattr(synID(line('.'), col('.'), 1), "name") if l:syn =~ b:commentRE " b:commentRE || l:syn =~ b:stringRE let l:iscomment = 1 endif call setpos('.', cursor) return l:iscomment endfunction " " Returns true if the given line is a comment line (b:lineCommentRE) " function! s:IsBlockComment(linenum) let cursor = getpos('.') exec a:linenum normal ^ let l:iscomment = 0 let l:syn = synIDattr(synID(line('.'), col('.'), 1), "name") if l:syn =~ b:blockCommentRE " b:commentRE || l:syn =~ b:stringRE let l:iscomment = 1 endif call setpos('.', cursor) return l:iscomment endfunction " " Get the first line at or on the given line that is not blank and is not a " comment line. " function! s:GetPrevNonBlankNonComment(begin) let cursor = getpos('.') let l:prevbegin = a:begin while 1 let l:lnum = prevnonblank(l:prevbegin) if l:lnum == 0 return 0 endif "if s:IsLineComment(l:lnum) if s:IsComment(l:lnum) let l:prevbegin -= 1 continue endif break endwhile " Restore original cursor location call setpos('.', cursor) return l:lnum endfunction " " This does all the work. Does indentation for: " " - All pairs defined in b:indentTrios " - All line continuations in b:lineContList " - Block comments " function! IndentAnything() if !exists('b:IndentAnythingInitialized') call s:IndentAnythingInit() endif let adj = 0 " Adjustment let g:lastindent = "" let b:hardindent = -1 let currlnum = v:lnum let currlnum = line('.') let currline = getline(currlnum) let lastline = '' let prevline = '' " Find non-blank lines above the current line. let lastlnum = prevnonblank(currlnum - 1) let prevlnum = prevnonblank(lastlnum - 1) if lastlnum != 0 let lastline = getline(lastlnum) endif if prevlnum != 0 let prevline = getline(prevlnum) endif if b:contTraversesLineComments let lastcodelnum = s:GetPrevNonBlankNonComment(currlnum - 1) let prevcodelnum = s:GetPrevNonBlankNonComment(lastcodelnum - 1) if lastcodelnum !=0 let lastcodeline = getline(lastcodelnum) endif endif " Start from the first char on the line. Vim doesn't seem to consistently " place the cursor there before calling the indent routines. call cursor(0, 1) call search('\S', 'W') let l:cur = getpos('.') " " Call indentation adjustment functions. " " " Block comments " let l:BlockCommentAdj = 0 let l:BlockCommentAdj += s:GetBlockCommentIndent(currlnum, lastlnum) let adj += l:BlockCommentAdj " " Pairs " let b:lastclosed = { 'at' : 0 } let b:pairadj = 0 if !l:BlockCommentAdj " If we're not in the middle of a block comment (because we haven't " made any adjustments for that), then process block indentation. for trio in b:indentTrios let b:pairadj += s:GetPairIndent(currline, lastline, lastlnum, \ trio[0], trio[1], trio[2]) endfor endif let adj += b:pairadj " " Line continuations " let contadj = 0 let isBlockCommentStart = currline =~ '^\s*' . b:blockCommentStartRE let isBlockCommentMid = (s:IsBlockComment(currlnum) && !isBlockCommentStart) if !isBlockCommentMid " If the current line is not the middle of a block comment, then " process line continuations. for ContRule in b:lineContList if b:contTraversesLineComments "&& !isBlockCommentStart let contadj = s:GetContIndent(ContRule, currline, lastcodeline, lastcodelnum, prevcodelnum) else let contadj = s:GetContIndent(ContRule, currline, lastline, lastlnum, prevlnum) endif " This is for line continuation patterns, of which there can be only " one per line to indicate continuation if contadj break endif endfor let adj += contadj endif " " Find the previous indent to which we will add the adjustment " let prevind = indent(lastlnum) if l:BlockCommentAdj let g:lastindent .= " indent (prevblockcomment: " . prevind . " at " . lastcodelnum . ") " elseif contadj && b:contTraversesLineComments " If we have adjusted for line continuation, then use the indentation " for the previous code line let prevind = indent(lastcodelnum) let g:lastindent .= " indent (prevcode: " . prevind . " at " . lastcodelnum . ") " elseif (isBlockCommentStart || !s:IsBlockComment(currlnum)) && s:IsBlockComment(lastlnum) " If this is the first line after a block comment, then add the " adjustment to the line where the block comment started. let prevind = s:GetPostBlockCommentIndent(lastlnum) let g:lastindent .= " indent (prevblock: " . prevind . " at " . lastlnum . ") " elseif exists("b:defaultIndentExpr") let g:lastindent .= " using defaultIndentExpr (" . b:defaultIndentExpr . ") " exec "let prevind = " . b:defaultIndentExpr else " Default to adjusting the previous line's indent. let g:lastindent .= " indent (prev: " . prevind . " at " . lastlnum . ") " endif " Just in case there is no previous indent. let prevind = (prevind == -1 ? 0 : prevind) if b:indent_anything_echo echom g:lastindent endif call setpos('.', l:cur) return adj + prevind endfunction " " Get the adjustment for the second line of a block comment. The second line " will be aligned under the start of the block, even if it is not at the " beginning of the line. Extra adjustment (b:blockCommentMiddleExtra) will " be added. " function! s:GetBlockCommentIndent(CurrLNum, LastLNum) let l:cursor = getpos('.') let l:adj = 0 if a:LastLNum == searchpair(b:blockCommentStartRE, '', b:blockCommentEndRE, 'bWr') \ && a:LastLNum > 0 let l:adj = col('.') + b:blockCommentMiddleExtra normal ^ let l:adj -= col('.') endif call setpos('.', l:cursor) return l:adj endfunction function! s:GetPostBlockCommentIndent(LNum) let l:cursor = getpos('.') let l:ind = 0 " Find beginning of block comment containing the start of line LNum exec a:LNum normal ^ let l:ind = indent(searchpair(b:blockCommentStartRE, '', b:blockCommentEndRE, 'bWr')) if 1 || l:ind != 0 && b:indent_anything_echo let g:lastindent = g:lastindent . \ "GetBlockCommentIndent: " . l:ind endif call setpos('.', l:cursor) "return l:ind return l:ind > 0 ? l:ind : 0 endfunction " EV ADDED " Function which determines if there are equal number of opening and closing " patterns on the supplied line. function! s:EqualPairs(Line, LNum, Head, Tail) let lnum = line('.') let cnum = col('.') call cursor(a:LNum, 1) try let head_matches = search('\(' . a:Head . '\)', 'ncp', a:LNum) let tail_matches = search('\(' . a:Tail . '\)', 'ncp', a:LNum) if head_matches == tail_matches call cursor(a:LNum, col('$')) let last_head = searchpos('\(' . a:Head . '\)', 'bcn', a:LNum) call cursor(a:LNum, col('$')) let last_tail = searchpos('\(' . a:Tail . '\)', 'bcn', a:LNum) return last_tail[1] > last_head[1] endif return 0 finally call cursor(lnum, cnum) endtry endfunction " " Get additional indentation based on blocks of code, as defined by the Head " and Tail patterns. " function! s:GetPairIndent(CurrLine, LastLine, LastLNum, Head, Mid, Tail) let levels = 0 let adj = 0 let origcol = col(".") let origline = line(".") " " How many levels were started on the last line? Search backwards for " pair starters until we're not on the last nonblank. If the last line " doesn't contain the pair-starter, then don't bother with searchpair(); " it's a performance bottleneck because (I think) it will always search " all the way back until it finds a match or can't search any more. " " if a:LastLine =~ a:Head while 1 " EV ADDED if s:EqualPairs(a:LastLine, a:LastLNum, a:Head, a:Tail) break endif " END EV ADDED " " Include the limit of the search to be the last line. BIG " performance booster! That also means we only have to see *if* " there was a match, and not worry about where it is. " "let pairstart = searchpair(a:Head, a:Mid, a:Tail, 'Wb') "if pairstart == 0 || pairstart != a:LastLNum let pairstart = searchpair(a:Head, a:Mid, a:Tail, 'Wb', '', a:LastLNum) if pairstart == 0 "|| pairstart != a:LastLNum break endif let syn = synIDattr(synID(line("."), col("."), 1), "name") " Also continue on the off chance that we find the match on the " current line. This shouldn't happen, but the pattern might " start with whitespace. if syn =~ b:commentRE || syn =~ b:stringRE || pairstart == origline continue endif let levels += 1 endwhile endif " If we aren't within a level that was started on the last line, then " check how many levels were closed on the last line. " if levels == 0 " Move to the beginning of the last line call cursor(a:LastLNum,0) normal ^ " If the line starts with an open, The close shouldn't be counted as " such, because we're looking for closes that didn't start on this " line. if a:LastLine =~ '^\s*' . a:Head || \ (a:Mid != '' && a:LastLine =~ '^\s*' . a:Mid) let levels = 1 endif " " Count the closes on the last line (i.e. LastLNum), stopping once " we've hit comments. If the line doesn't even contain the end of the " pair, don't bother with searchpair() (same aforementioned " rationale). " if a:LastLine =~ a:Tail while 1 " " Include the limit of the search to be the last line. BIG " performance booster! That also means we only have to see " *if* there was a match, and not worry about where it is. " "let pairend = searchpair(a:Head, a:Mid, a:Tail, 'W') "if pairend == 0 || a:LastLNum != pairend "let pairend = searchpair(a:Head, a:Mid, a:Tail, 'W', '', a:LastLNum) let pairend = searchpair(a:Head, a:Mid, a:Tail, 'W', \'InCommentOrString()', a:LastLNum) if pairend == 0 "|| a:LastLNum != pairend " STARTS with a:Tail, since we already know the line " matches it. if b:lastclosed.at < col('.') && ( \ a:LastLine =~ '^\s*' . a:Tail \ || (a:Mid != '' && a:LastLine =~ '^\s*' . a:Mid) ) let b:lastclosed = { \ 'at' : col('.'), \ 'head' : a:Head, \ 'mid' : a:Mid, \ 'tail' : a:Tail } endif break endif " This might not be needed with the expr included in the " search call. "let syn = synIDattr(synID(line("."), col("."), 1), "name") "if syn =~ b:commentRE || syn =~ b:stringRE || syn == '' " break "endif let levels -= 1 " Track the last close to try to match pairs that start on " line continuations if b:lastclosed.at < col('.') let b:lastclosed = { \ 'at' : col('.'), \ 'head' : a:Head, \ 'mid' : a:Mid, \ 'tail' : a:Tail } endif endwhile endif endif " This is redundant, as per above " If the current line starts with a close, count it. It won't effect the " indentation of the next line because it is the first thing on the line " and won't be counted as a "close on the last line". if a:CurrLine =~ '^\s*' . a:Tail \ || (a:Mid != '' && a:CurrLine =~ '^\s*' . a:Mid) let levels -= 1 endif " Restore original cursor location call cursor(origline, origcol) let adj = &sw*levels if adj != 0 && b:indent_anything_echo let g:lastindent = g:lastindent . \ "GetPairIndent(" . a:Head . "/" . b:lastclosed.at . "):" . adj . " " endif return adj endfunction function! s:GetContIndent(Rule, CurrLine, LastLine, LastLNum, PrevLNum) let adj = 0 let origcol = col(".") let origline = line(".") let lastcont = 0 let prevcont = 0 let l:lastlnum = a:LastLNum let l:prevlnum = a:PrevLNum let l:preblockstart = -1 " Get the last matching line number. If the match occurs w/in a comment " or string, then it's a non-match. " "let lastmatchlnum = search(a:Rule.pattern, 'Wb', a:PrevLNum) let lastmatchlnum = search(a:Rule.pattern, 'Wb', a:LastLNum) let syn = synIDattr(synID(line("."), col("."), 1), "name") "if syn =~ b:commentRE || syn =~ b:stringRE if syn =~ b:commentRE || syn =~ b:stringRE || b:lastclosed.at > 0 let lastmatchlnum = 0 endif " Should be able to just search to the line.... " " Figure out the last and previous continuation status " if lastmatchlnum && lastmatchlnum == a:LastLNum " let lastcont = 1 " endif if lastmatchlnum == a:LastLNum let lastcont = 1 endif " start checking at the start of the block that ended on the prev line if b:lastclosed.at > 0 call cursor(a:LastLNum, b:lastclosed.at) " TODO: add 'skip' to skip comments let l:preblockstart = searchpair(b:lastclosed.head, b:lastclosed.mid, b:lastclosed.tail, 'bW') let g:lastindent .= ' postpair ("' . b:lastclosed.head . '"): ' \ . l:preblockstart . '/' . col('.') . ' ' if b:contTraversesLineComments let l:prevlnum = s:GetPrevNonBlankNonComment(line('.') - 1) else let l:prevlnum = prevnonblank(line('.') - 1) endif endif " Get the previous matching line number. If the match occurs w/in a " comment or string, then it's a non-match. Use the adjusted, local " prevlnum as the limit of the search, since we don't care about matches " beyond that. let prevmatchlnum = search(a:Rule.pattern, 'Wb', l:prevlnum) let syn = synIDattr(synID(line("."), col("."), 1), "name") " Handle: " if () " if () { " this_line; // should not be reduced "if syn =~ b:commentRE || syn =~ b:stringRE if syn =~ b:commentRE || syn =~ b:stringRE let prevmatchlnum = 0 endif " Should be able to just search to the line.... " if ( lastmatchlnum && lastmatchlnum == a:PrevLNum ) " \ || ( prevmatchlnum && prevmatchlnum == l:prevlnum ) " let prevcont = 1 " endif " " If there is a previous line, it is a continued line, and we haven't " already done a positive adjustment for a pair/block, then reduce. " Don't undo a positive adjustment for a pair because the previous line " was a continued line. That will happen after the end of the block. "if prevmatchlnum == l:prevlnum && b:pairadj <= 0 if l:prevlnum && prevmatchlnum == l:prevlnum && b:pairadj <= 0 let prevcont = 1 endif "echom "lastcont: " . lastcont . " \ ", prevcont: " . prevcont . " \ ", lastmatchlnum: " . lastmatchlnum . " \ ", prevmatchlnum: " . prevmatchlnum . " \ ", lastlnum: " . a:LastLNum . " \ ", PrevLNum: " . a:PrevLNum let firstcont = (lastcont && !prevcont) let firstcont = ((lastcont && !prevcont) || (lastcont && b:pairadj)) " If we are adjusting the current line for a pair, then don't count this " line as a post-continuation line. The post continuation line will be " after the close of said pair. let postcont = (!lastcont && prevcont) "let postcont = (!lastcont && prevcont && !b:pairadj ) let g:lastindent .= 'lastcont (' . lastcont . '), prevcont (' . prevcont . ') ' "if firstcont && a:CurrLine !~ '^\s*{' if firstcont if has_key(a:Rule, 'ignore') && a:CurrLine =~ a:Rule.ignore let g:lastindent .= "(ignoring '" . a:Rule.ignore . "') " else let adj = adj + &sw endif "elseif postcont && a:LastLine !~ '^\s*{' "&& !b:pairadj elseif postcont if has_key(a:Rule, 'ignore') && a:LastLine =~ a:Rule.ignore let g:lastindent .= "(ignoring '" . a:Rule.ignore . "') " else let adj = adj - &sw endif endif call cursor(origline, origcol) if adj != 0 && b:indent_anything_echo let g:lastindent = g:lastindent . \ "GetContIndent('" . a:Rule.pattern . "'):" . adj . " " endif return adj endfunction