typescript.vim 14 KB

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