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