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