123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- " Vim indent file
- " Language: Solidity
- " Maintainer: Cothi (jiungdev@gmail.com)
- " Original Author: tomlion (https://github.com/tomlion/vim-solidity)
- " Last Change: 2022 Sep 27
- " 2023 Aug 22 Vim Project (undo_indent)
- "
- " Acknowledgement: Based off of vim-javascript
- "
- " 0. Initialization {{{1
- " =================
- " Only load this indent file when no other was loaded.
- if exists("b:did_indent")
- finish
- endif
- let b:did_indent = 1
- setlocal nosmartindent
- " Now, set up our indentation expression and keys that trigger it.
- setlocal indentexpr=GetSolidityIndent()
- setlocal indentkeys=0{,0},0),0],0\,,!^F,o,O,e
- let b:undo_indent = "setlocal indentexpr< indentkeys< smartindent<"
- " Only define the function once.
- if exists("*GetSolidityIndent")
- finish
- endif
- let s:cpo_save = &cpo
- set cpo&vim
- " 1. Variables {{{1
- " ============
- let s:js_keywords = '^\s*\(break\|case\|catch\|continue\|debugger\|default\|delete\|do\|else\|finally\|for\|function\|if\|in\|instanceof\|new\|return\|switch\|this\|throw\|try\|typeof\|var\|void\|while\|with\)'
- " Regex of syntax group names that are or delimit string or are comments.
- let s:syng_strcom = 'string\|regex\|comment\c'
- " Regex of syntax group names that are strings.
- let s:syng_string = 'regex\c'
- " Regex of syntax group names that are strings or documentation.
- let s:syng_multiline = 'comment\c'
- " Regex of syntax group names that are line comment.
- let s:syng_linecom = 'linecomment\c'
- " Expression used to check whether we should skip a match with searchpair().
- let s:skip_expr = "synIDattr(synID(line('.'),col('.'),1),'name') =~ '".s:syng_strcom."'"
- let s:line_term = '\s*\%(\%(\/\/\).*\)\=$'
- " Regex that defines continuation lines, not including (, {, or [.
- let s:continuation_regex = '\%([\\*+/.:]\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)' . s:line_term
- " Regex that defines continuation lines.
- " TODO: this needs to deal with if ...: and so on
- let s:msl_regex = '\%([\\*+/.:([]\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)' . s:line_term
- let s:one_line_scope_regex = '\<\%(if\|else\|for\|while\)\>[^{;]*' . s:line_term
- " Regex that defines blocks.
- let s:block_regex = '\%([{[]\)\s*\%(|\%([*@]\=\h\w*,\=\s*\)\%(,\s*[*@]\=\h\w*\)*|\)\=' . s:line_term
- let s:var_stmt = '^\s*var'
- let s:comma_first = '^\s*,'
- let s:comma_last = ',\s*$'
- let s:ternary = '^\s\+[?|:]'
- let s:ternary_q = '^\s\+?'
- " 2. Auxiliary Functions {{{1
- " ======================
- " Check if the character at lnum:col is inside a string, comment, or is ascii.
- function s:IsInStringOrComment(lnum, col)
- return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_strcom
- endfunction
- " Check if the character at lnum:col is inside a string.
- function s:IsInString(lnum, col)
- return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_string
- endfunction
- " Check if the character at lnum:col is inside a multi-line comment.
- function s:IsInMultilineComment(lnum, col)
- return !s:IsLineComment(a:lnum, a:col) && synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_multiline
- endfunction
- " Check if the character at lnum:col is a line comment.
- function s:IsLineComment(lnum, col)
- return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_linecom
- endfunction
- " Find line above 'lnum' that isn't empty, in a comment, or in a string.
- function s:PrevNonBlankNonString(lnum)
- let in_block = 0
- let lnum = prevnonblank(a:lnum)
- while lnum > 0
- " Go in and out of blocks comments as necessary.
- " If the line isn't empty (with opt. comment) or in a string, end search.
- let line = getline(lnum)
- if line =~ '/\*'
- if in_block
- let in_block = 0
- else
- break
- endif
- elseif !in_block && line =~ '\*/'
- let in_block = 1
- elseif !in_block && line !~ '^\s*\%(//\).*$' && !(s:IsInStringOrComment(lnum, 1) && s:IsInStringOrComment(lnum, strlen(line)))
- break
- endif
- let lnum = prevnonblank(lnum - 1)
- endwhile
- return lnum
- endfunction
- " Find line above 'lnum' that started the continuation 'lnum' may be part of.
- function s:GetMSL(lnum, in_one_line_scope)
- " Start on the line we're at and use its indent.
- let msl = a:lnum
- let lnum = s:PrevNonBlankNonString(a:lnum - 1)
- while lnum > 0
- " If we have a continuation line, or we're in a string, use line as MSL.
- " Otherwise, terminate search as we have found our MSL already.
- let line = getline(lnum)
- let col = match(line, s:msl_regex) + 1
- if (col > 0 && !s:IsInStringOrComment(lnum, col)) || s:IsInString(lnum, strlen(line))
- let msl = lnum
- else
- " Don't use lines that are part of a one line scope as msl unless the
- " flag in_one_line_scope is set to 1
- "
- if a:in_one_line_scope
- break
- end
- let msl_one_line = s:Match(lnum, s:one_line_scope_regex)
- if msl_one_line == 0
- break
- endif
- endif
- let lnum = s:PrevNonBlankNonString(lnum - 1)
- endwhile
- return msl
- endfunction
- function s:RemoveTrailingComments(content)
- let single = '\/\/\(.*\)\s*$'
- let multi = '\/\*\(.*\)\*\/\s*$'
- return substitute(substitute(a:content, single, '', ''), multi, '', '')
- endfunction
- " Find if the string is inside var statement (but not the first string)
- function s:InMultiVarStatement(lnum)
- let lnum = s:PrevNonBlankNonString(a:lnum - 1)
- " let type = synIDattr(synID(lnum, indent(lnum) + 1, 0), 'name')
- " loop through previous expressions to find a var statement
- while lnum > 0
- let line = getline(lnum)
- " if the line is a js keyword
- if (line =~ s:js_keywords)
- " check if the line is a var stmt
- " if the line has a comma first or comma last then we can assume that we
- " are in a multiple var statement
- if (line =~ s:var_stmt)
- return lnum
- endif
- " other js keywords, not a var
- return 0
- endif
- let lnum = s:PrevNonBlankNonString(lnum - 1)
- endwhile
- " beginning of program, not a var
- return 0
- endfunction
- " Find line above with beginning of the var statement or returns 0 if it's not
- " this statement
- function s:GetVarIndent(lnum)
- let lvar = s:InMultiVarStatement(a:lnum)
- let prev_lnum = s:PrevNonBlankNonString(a:lnum - 1)
- if lvar
- let line = s:RemoveTrailingComments(getline(prev_lnum))
- " if the previous line doesn't end in a comma, return to regular indent
- if (line !~ s:comma_last)
- return indent(prev_lnum) - &sw
- else
- return indent(lvar) + &sw
- endif
- endif
- return -1
- endfunction
- " Check if line 'lnum' has more opening brackets than closing ones.
- function s:LineHasOpeningBrackets(lnum)
- let open_0 = 0
- let open_2 = 0
- let open_4 = 0
- let line = getline(a:lnum)
- let pos = match(line, '[][(){}]', 0)
- while pos != -1
- if !s:IsInStringOrComment(a:lnum, pos + 1)
- let idx = stridx('(){}[]', line[pos])
- if idx % 2 == 0
- let open_{idx} = open_{idx} + 1
- else
- let open_{idx - 1} = open_{idx - 1} - 1
- endif
- endif
- let pos = match(line, '[][(){}]', pos + 1)
- endwhile
- return (open_0 > 0) . (open_2 > 0) . (open_4 > 0)
- endfunction
- function s:Match(lnum, regex)
- let col = match(getline(a:lnum), a:regex) + 1
- return col > 0 && !s:IsInStringOrComment(a:lnum, col) ? col : 0
- endfunction
- function s:IndentWithContinuation(lnum, ind, width)
- " Set up variables to use and search for MSL to the previous line.
- let p_lnum = a:lnum
- let lnum = s:GetMSL(a:lnum, 1)
- let line = getline(lnum)
- " If the previous line wasn't a MSL and is continuation return its indent.
- " TODO: the || s:IsInString() thing worries me a bit.
- if p_lnum != lnum
- if s:Match(p_lnum,s:continuation_regex)||s:IsInString(p_lnum,strlen(line))
- return a:ind
- endif
- endif
- " Set up more variables now that we know we aren't continuation bound.
- let msl_ind = indent(lnum)
- " If the previous line ended with [*+/.-=], start a continuation that
- " indents an extra level.
- if s:Match(lnum, s:continuation_regex)
- if lnum == p_lnum
- return msl_ind + a:width
- else
- return msl_ind
- endif
- endif
- return a:ind
- endfunction
- function s:InOneLineScope(lnum)
- let msl = s:GetMSL(a:lnum, 1)
- if msl > 0 && s:Match(msl, s:one_line_scope_regex)
- return msl
- endif
- return 0
- endfunction
- function s:ExitingOneLineScope(lnum)
- let msl = s:GetMSL(a:lnum, 1)
- if msl > 0
- " if the current line is in a one line scope ..
- if s:Match(msl, s:one_line_scope_regex)
- return 0
- else
- let prev_msl = s:GetMSL(msl - 1, 1)
- if s:Match(prev_msl, s:one_line_scope_regex)
- return prev_msl
- endif
- endif
- endif
- return 0
- endfunction
- " 3. GetSolidityIndent Function {{{1
- " =========================
- function GetSolidityIndent()
- " 3.1. Setup {{{2
- " ----------
- " Set up variables for restoring position in file. Could use v:lnum here.
- let vcol = col('.')
- " 3.2. Work on the current line {{{2
- " -----------------------------
- let ind = -1
- " Get the current line.
- let line = getline(v:lnum)
- " previous nonblank line number
- let prevline = prevnonblank(v:lnum - 1)
- " If we got a closing bracket on an empty line, find its match and indent
- " according to it. For parentheses we indent to its column - 1, for the
- " others we indent to the containing line's MSL's level. Return -1 if fail.
- let col = matchend(line, '^\s*[],})]')
- if col > 0 && !s:IsInStringOrComment(v:lnum, col)
- call cursor(v:lnum, col)
- let lvar = s:InMultiVarStatement(v:lnum)
- if lvar
- let prevline_contents = s:RemoveTrailingComments(getline(prevline))
- " check for comma first
- if (line[col - 1] =~ ',')
- " if the previous line ends in comma or semicolon don't indent
- if (prevline_contents =~ '[;,]\s*$')
- return indent(s:GetMSL(line('.'), 0))
- " get previous line indent, if it's comma first return prevline indent
- elseif (prevline_contents =~ s:comma_first)
- return indent(prevline)
- " otherwise we indent 1 level
- else
- return indent(lvar) + &sw
- endif
- endif
- endif
- let bs = strpart('(){}[]', stridx(')}]', line[col - 1]) * 2, 2)
- if searchpair(escape(bs[0], '\['), '', bs[1], 'bW', s:skip_expr) > 0
- if line[col-1]==')' && col('.') != col('$') - 1
- let ind = virtcol('.')-1
- else
- let ind = indent(s:GetMSL(line('.'), 0))
- endif
- endif
- return ind
- endif
- " If the line is comma first, dedent 1 level
- if (getline(prevline) =~ s:comma_first)
- return indent(prevline) - &sw
- endif
- if (line =~ s:ternary)
- if (getline(prevline) =~ s:ternary_q)
- return indent(prevline)
- else
- return indent(prevline) + &sw
- endif
- endif
- " If we are in a multi-line comment, cindent does the right thing.
- if s:IsInMultilineComment(v:lnum, 1) && !s:IsLineComment(v:lnum, 1)
- return cindent(v:lnum)
- endif
- " Check for multiple var assignments
- " let var_indent = s:GetVarIndent(v:lnum)
- " if var_indent >= 0
- " return var_indent
- " endif
- " 3.3. Work on the previous line. {{{2
- " -------------------------------
- " If the line is empty and the previous nonblank line was a multi-line
- " comment, use that comment's indent. Deduct one char to account for the
- " space in ' */'.
- if line =~ '^\s*$' && s:IsInMultilineComment(prevline, 1)
- return indent(prevline) - 1
- endif
- " Find a non-blank, non-multi-line string line above the current line.
- let lnum = s:PrevNonBlankNonString(v:lnum - 1)
- " If the line is empty and inside a string, use the previous line.
- if line =~ '^\s*$' && lnum != prevline
- return indent(prevnonblank(v:lnum))
- endif
- " At the start of the file use zero indent.
- if lnum == 0
- return 0
- endif
- " Set up variables for current line.
- let line = getline(lnum)
- let ind = indent(lnum)
- " If the previous line ended with a block opening, add a level of indent.
- if s:Match(lnum, s:block_regex)
- return indent(s:GetMSL(lnum, 0)) + &sw
- endif
- " If the previous line contained an opening bracket, and we are still in it,
- " add indent depending on the bracket type.
- if line =~ '[[({]'
- let counts = s:LineHasOpeningBrackets(lnum)
- if counts[0] == '1' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
- if col('.') + 1 == col('$')
- return ind + &sw
- else
- return virtcol('.')
- endif
- elseif counts[1] == '1' || counts[2] == '1'
- return ind + &sw
- else
- call cursor(v:lnum, vcol)
- end
- endif
- " 3.4. Work on the MSL line. {{{2
- " --------------------------
- let ind_con = ind
- let ind = s:IndentWithContinuation(lnum, ind_con, &sw)
- " }}}2
- "
- "
- let ols = s:InOneLineScope(lnum)
- if ols > 0
- let ind = ind + &sw
- else
- let ols = s:ExitingOneLineScope(lnum)
- while ols > 0 && ind > 0
- let ind = ind - &sw
- let ols = s:InOneLineScope(ols - 1)
- endwhile
- endif
- return ind
- endfunction
- " }}}1
- let &cpo = s:cpo_save
- unlet s:cpo_save
|