neovim

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

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