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