neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

solidity.vim (12913B)


      1 " Vim indent file
      2 " Language:		Solidity
      3 " Maintainer:		Cothi (jiungdev@gmail.com)
      4 " Original Author:	tomlion (https://github.com/tomlion/vim-solidity)
      5 " Last Change:		2022 Sep 27
      6 " 			2023 Aug 22 Vim Project (undo_indent)
      7 "
      8 " Acknowledgement: Based off of vim-javascript
      9 "
     10 " 0. Initialization {{{1
     11 " =================
     12 
     13 " Only load this indent file when no other was loaded.
     14 if exists("b:did_indent")
     15  finish
     16 endif
     17 let b:did_indent = 1
     18 
     19 setlocal nosmartindent
     20 
     21 " Now, set up our indentation expression and keys that trigger it.
     22 setlocal indentexpr=GetSolidityIndent()
     23 setlocal indentkeys=0{,0},0),0],0\,,!^F,o,O,e
     24 
     25 let b:undo_indent = "setlocal indentexpr< indentkeys< smartindent<"
     26 
     27 " Only define the function once.
     28 if exists("*GetSolidityIndent")
     29  finish
     30 endif
     31 
     32 let s:cpo_save = &cpo
     33 set cpo&vim
     34 
     35 " 1. Variables {{{1
     36 " ============
     37 
     38 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\)'
     39 
     40 " Regex of syntax group names that are or delimit string or are comments.
     41 let s:syng_strcom = 'string\|regex\|comment\c'
     42 
     43 " Regex of syntax group names that are strings.
     44 let s:syng_string = 'regex\c'
     45 
     46 " Regex of syntax group names that are strings or documentation.
     47 let s:syng_multiline = 'comment\c'
     48 
     49 " Regex of syntax group names that are line comment.
     50 let s:syng_linecom = 'linecomment\c'
     51 
     52 " Expression used to check whether we should skip a match with searchpair().
     53 let s:skip_expr = "synIDattr(synID(line('.'),col('.'),1),'name') =~ '".s:syng_strcom."'"
     54 
     55 let s:line_term = '\s*\%(\%(\/\/\).*\)\=$'
     56 
     57 " Regex that defines continuation lines, not including (, {, or [.
     58 let s:continuation_regex = '\%([\\*+/.:]\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)' . s:line_term
     59 
     60 " Regex that defines continuation lines.
     61 " TODO: this needs to deal with if ...: and so on
     62 let s:msl_regex = '\%([\\*+/.:([]\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)' . s:line_term
     63 
     64 let s:one_line_scope_regex = '\<\%(if\|else\|for\|while\)\>[^{;]*' . s:line_term
     65 
     66 " Regex that defines blocks.
     67 let s:block_regex = '\%([{[]\)\s*\%(|\%([*@]\=\h\w*,\=\s*\)\%(,\s*[*@]\=\h\w*\)*|\)\=' . s:line_term
     68 
     69 let s:var_stmt = '^\s*var'
     70 
     71 let s:comma_first = '^\s*,'
     72 let s:comma_last = ',\s*$'
     73 
     74 let s:ternary = '^\s\+[?|:]'
     75 let s:ternary_q = '^\s\+?'
     76 
     77 " 2. Auxiliary Functions {{{1
     78 " ======================
     79 
     80 " Check if the character at lnum:col is inside a string, comment, or is ascii.
     81 function s:IsInStringOrComment(lnum, col)
     82  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_strcom
     83 endfunction
     84 
     85 " Check if the character at lnum:col is inside a string.
     86 function s:IsInString(lnum, col)
     87  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_string
     88 endfunction
     89 
     90 " Check if the character at lnum:col is inside a multi-line comment.
     91 function s:IsInMultilineComment(lnum, col)
     92  return !s:IsLineComment(a:lnum, a:col) && synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_multiline
     93 endfunction
     94 
     95 " Check if the character at lnum:col is a line comment.
     96 function s:IsLineComment(lnum, col)
     97  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_linecom
     98 endfunction
     99 
    100 " Find line above 'lnum' that isn't empty, in a comment, or in a string.
    101 function s:PrevNonBlankNonString(lnum)
    102  let in_block = 0
    103  let lnum = prevnonblank(a:lnum)
    104  while lnum > 0
    105    " Go in and out of blocks comments as necessary.
    106    " If the line isn't empty (with opt. comment) or in a string, end search.
    107    let line = getline(lnum)
    108    if line =~ '/\*'
    109      if in_block
    110        let in_block = 0
    111      else
    112        break
    113      endif
    114    elseif !in_block && line =~ '\*/'
    115      let in_block = 1
    116    elseif !in_block && line !~ '^\s*\%(//\).*$' && !(s:IsInStringOrComment(lnum, 1) && s:IsInStringOrComment(lnum, strlen(line)))
    117      break
    118    endif
    119    let lnum = prevnonblank(lnum - 1)
    120  endwhile
    121  return lnum
    122 endfunction
    123 
    124 " Find line above 'lnum' that started the continuation 'lnum' may be part of.
    125 function s:GetMSL(lnum, in_one_line_scope)
    126  " Start on the line we're at and use its indent.
    127  let msl = a:lnum
    128  let lnum = s:PrevNonBlankNonString(a:lnum - 1)
    129  while lnum > 0
    130    " If we have a continuation line, or we're in a string, use line as MSL.
    131    " Otherwise, terminate search as we have found our MSL already.
    132    let line = getline(lnum)
    133    let col = match(line, s:msl_regex) + 1
    134    if (col > 0 && !s:IsInStringOrComment(lnum, col)) || s:IsInString(lnum, strlen(line))
    135      let msl = lnum
    136    else
    137      " Don't use lines that are part of a one line scope as msl unless the
    138      " flag in_one_line_scope is set to 1
    139      "
    140      if a:in_one_line_scope
    141        break
    142      end
    143      let msl_one_line = s:Match(lnum, s:one_line_scope_regex)
    144      if msl_one_line == 0
    145        break
    146      endif
    147    endif
    148    let lnum = s:PrevNonBlankNonString(lnum - 1)
    149  endwhile
    150  return msl
    151 endfunction
    152 
    153 function s:RemoveTrailingComments(content)
    154  let single = '\/\/\(.*\)\s*$'
    155  let multi = '\/\*\(.*\)\*\/\s*$'
    156  return substitute(substitute(a:content, single, '', ''), multi, '', '')
    157 endfunction
    158 
    159 " Find if the string is inside var statement (but not the first string)
    160 function s:InMultiVarStatement(lnum)
    161  let lnum = s:PrevNonBlankNonString(a:lnum - 1)
    162 
    163 "  let type = synIDattr(synID(lnum, indent(lnum) + 1, 0), 'name')
    164 
    165  " loop through previous expressions to find a var statement
    166  while lnum > 0
    167    let line = getline(lnum)
    168 
    169    " if the line is a js keyword
    170    if (line =~ s:js_keywords)
    171      " check if the line is a var stmt
    172      " if the line has a comma first or comma last then we can assume that we
    173      " are in a multiple var statement
    174      if (line =~ s:var_stmt)
    175        return lnum
    176      endif
    177 
    178      " other js keywords, not a var
    179      return 0
    180    endif
    181 
    182    let lnum = s:PrevNonBlankNonString(lnum - 1)
    183  endwhile
    184 
    185  " beginning of program, not a var
    186  return 0
    187 endfunction
    188 
    189 " Find line above with beginning of the var statement or returns 0 if it's not
    190 " this statement
    191 function s:GetVarIndent(lnum)
    192  let lvar = s:InMultiVarStatement(a:lnum)
    193  let prev_lnum = s:PrevNonBlankNonString(a:lnum - 1)
    194 
    195  if lvar
    196    let line = s:RemoveTrailingComments(getline(prev_lnum))
    197 
    198    " if the previous line doesn't end in a comma, return to regular indent
    199    if (line !~ s:comma_last)
    200      return indent(prev_lnum) - &sw
    201    else
    202      return indent(lvar) + &sw
    203    endif
    204  endif
    205 
    206  return -1
    207 endfunction
    208 
    209 
    210 " Check if line 'lnum' has more opening brackets than closing ones.
    211 function s:LineHasOpeningBrackets(lnum)
    212  let open_0 = 0
    213  let open_2 = 0
    214  let open_4 = 0
    215  let line = getline(a:lnum)
    216  let pos = match(line, '[][(){}]', 0)
    217  while pos != -1
    218    if !s:IsInStringOrComment(a:lnum, pos + 1)
    219      let idx = stridx('(){}[]', line[pos])
    220      if idx % 2 == 0
    221        let open_{idx} = open_{idx} + 1
    222      else
    223        let open_{idx - 1} = open_{idx - 1} - 1
    224      endif
    225    endif
    226    let pos = match(line, '[][(){}]', pos + 1)
    227  endwhile
    228  return (open_0 > 0) . (open_2 > 0) . (open_4 > 0)
    229 endfunction
    230 
    231 function s:Match(lnum, regex)
    232  let col = match(getline(a:lnum), a:regex) + 1
    233  return col > 0 && !s:IsInStringOrComment(a:lnum, col) ? col : 0
    234 endfunction
    235 
    236 function s:IndentWithContinuation(lnum, ind, width)
    237  " Set up variables to use and search for MSL to the previous line.
    238  let p_lnum = a:lnum
    239  let lnum = s:GetMSL(a:lnum, 1)
    240  let line = getline(lnum)
    241 
    242  " If the previous line wasn't a MSL and is continuation return its indent.
    243  " TODO: the || s:IsInString() thing worries me a bit.
    244  if p_lnum != lnum
    245    if s:Match(p_lnum,s:continuation_regex)||s:IsInString(p_lnum,strlen(line))
    246      return a:ind
    247    endif
    248  endif
    249 
    250  " Set up more variables now that we know we aren't continuation bound.
    251  let msl_ind = indent(lnum)
    252 
    253  " If the previous line ended with [*+/.-=], start a continuation that
    254  " indents an extra level.
    255  if s:Match(lnum, s:continuation_regex)
    256    if lnum == p_lnum
    257      return msl_ind + a:width
    258    else
    259      return msl_ind
    260    endif
    261  endif
    262 
    263  return a:ind
    264 endfunction
    265 
    266 function s:InOneLineScope(lnum)
    267  let msl = s:GetMSL(a:lnum, 1)
    268  if msl > 0 && s:Match(msl, s:one_line_scope_regex)
    269    return msl
    270  endif
    271  return 0
    272 endfunction
    273 
    274 function s:ExitingOneLineScope(lnum)
    275  let msl = s:GetMSL(a:lnum, 1)
    276  if msl > 0
    277    " if the current line is in a one line scope ..
    278    if s:Match(msl, s:one_line_scope_regex)
    279      return 0
    280    else
    281      let prev_msl = s:GetMSL(msl - 1, 1)
    282      if s:Match(prev_msl, s:one_line_scope_regex)
    283        return prev_msl
    284      endif
    285    endif
    286  endif
    287  return 0
    288 endfunction
    289 
    290 " 3. GetSolidityIndent Function {{{1
    291 " =========================
    292 
    293 function GetSolidityIndent()
    294  " 3.1. Setup {{{2
    295  " ----------
    296 
    297  " Set up variables for restoring position in file.  Could use v:lnum here.
    298  let vcol = col('.')
    299 
    300  " 3.2. Work on the current line {{{2
    301  " -----------------------------
    302 
    303  let ind = -1
    304  " Get the current line.
    305  let line = getline(v:lnum)
    306  " previous nonblank line number
    307  let prevline = prevnonblank(v:lnum - 1)
    308 
    309  " If we got a closing bracket on an empty line, find its match and indent
    310  " according to it.  For parentheses we indent to its column - 1, for the
    311  " others we indent to the containing line's MSL's level.  Return -1 if fail.
    312  let col = matchend(line, '^\s*[],})]')
    313  if col > 0 && !s:IsInStringOrComment(v:lnum, col)
    314    call cursor(v:lnum, col)
    315 
    316    let lvar = s:InMultiVarStatement(v:lnum)
    317    if lvar
    318      let prevline_contents = s:RemoveTrailingComments(getline(prevline))
    319 
    320      " check for comma first
    321      if (line[col - 1] =~ ',')
    322        " if the previous line ends in comma or semicolon don't indent
    323        if (prevline_contents =~ '[;,]\s*$')
    324          return indent(s:GetMSL(line('.'), 0))
    325        " get previous line indent, if it's comma first return prevline indent
    326        elseif (prevline_contents =~ s:comma_first)
    327          return indent(prevline)
    328        " otherwise we indent 1 level
    329        else
    330          return indent(lvar) + &sw
    331        endif
    332      endif
    333    endif
    334 
    335 
    336    let bs = strpart('(){}[]', stridx(')}]', line[col - 1]) * 2, 2)
    337    if searchpair(escape(bs[0], '\['), '', bs[1], 'bW', s:skip_expr) > 0
    338      if line[col-1]==')' && col('.') != col('$') - 1
    339        let ind = virtcol('.')-1
    340      else
    341        let ind = indent(s:GetMSL(line('.'), 0))
    342      endif
    343    endif
    344    return ind
    345  endif
    346 
    347  " If the line is comma first, dedent 1 level
    348  if (getline(prevline) =~ s:comma_first)
    349    return indent(prevline) - &sw
    350  endif
    351 
    352  if (line =~ s:ternary)
    353    if (getline(prevline) =~ s:ternary_q)
    354      return indent(prevline)
    355    else
    356      return indent(prevline) + &sw
    357    endif
    358  endif
    359 
    360  " If we are in a multi-line comment, cindent does the right thing.
    361  if s:IsInMultilineComment(v:lnum, 1) && !s:IsLineComment(v:lnum, 1)
    362    return cindent(v:lnum)
    363  endif
    364 
    365  " Check for multiple var assignments
    366 "  let var_indent = s:GetVarIndent(v:lnum)
    367 "  if var_indent >= 0
    368 "    return var_indent
    369 "  endif
    370 
    371  " 3.3. Work on the previous line. {{{2
    372  " -------------------------------
    373 
    374  " If the line is empty and the previous nonblank line was a multi-line
    375  " comment, use that comment's indent. Deduct one char to account for the
    376  " space in ' */'.
    377  if line =~ '^\s*$' && s:IsInMultilineComment(prevline, 1)
    378    return indent(prevline) - 1
    379  endif
    380 
    381  " Find a non-blank, non-multi-line string line above the current line.
    382  let lnum = s:PrevNonBlankNonString(v:lnum - 1)
    383 
    384  " If the line is empty and inside a string, use the previous line.
    385  if line =~ '^\s*$' && lnum != prevline
    386    return indent(prevnonblank(v:lnum))
    387  endif
    388 
    389  " At the start of the file use zero indent.
    390  if lnum == 0
    391    return 0
    392  endif
    393 
    394  " Set up variables for current line.
    395  let line = getline(lnum)
    396  let ind = indent(lnum)
    397 
    398  " If the previous line ended with a block opening, add a level of indent.
    399  if s:Match(lnum, s:block_regex)
    400    return indent(s:GetMSL(lnum, 0)) + &sw
    401  endif
    402 
    403  " If the previous line contained an opening bracket, and we are still in it,
    404  " add indent depending on the bracket type.
    405  if line =~ '[[({]'
    406    let counts = s:LineHasOpeningBrackets(lnum)
    407    if counts[0] == '1' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
    408      if col('.') + 1 == col('$')
    409        return ind + &sw
    410      else
    411        return virtcol('.')
    412      endif
    413    elseif counts[1] == '1' || counts[2] == '1'
    414      return ind + &sw
    415    else
    416      call cursor(v:lnum, vcol)
    417    end
    418  endif
    419 
    420  " 3.4. Work on the MSL line. {{{2
    421  " --------------------------
    422 
    423  let ind_con = ind
    424  let ind = s:IndentWithContinuation(lnum, ind_con, &sw)
    425 
    426  " }}}2
    427  "
    428  "
    429  let ols = s:InOneLineScope(lnum)
    430  if ols > 0
    431    let ind = ind + &sw
    432  else
    433    let ols = s:ExitingOneLineScope(lnum)
    434    while ols > 0 && ind > 0
    435      let ind = ind - &sw
    436      let ols = s:InOneLineScope(ols - 1)
    437    endwhile
    438  endif
    439 
    440  return ind
    441 endfunction
    442 
    443 " }}}1
    444 
    445 let &cpo = s:cpo_save
    446 unlet s:cpo_save