solidity.vim 13 KB

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