1" Vim indent file
2" Language: Javascript
3" Maintainer: Chris Paul ( https://github.com/bounceme )
4" URL: https://github.com/pangloss/vim-javascript
5" Last Change: December 4, 2017
6
7" Only load this indent file when no other was loaded.
8if exists('b:did_indent')
9  finish
10endif
11let b:did_indent = 1
12
13" Now, set up our indentation expression and keys that trigger it.
14setlocal indentexpr=GetJavascriptIndent()
15setlocal autoindent nolisp nosmartindent
16setlocal indentkeys+=0],0)
17" Testable with something like:
18" vim  -eNs "+filetype plugin indent on" "+syntax on" "+set ft=javascript" \
19"       "+norm! gg=G" '+%print' '+:q!' testfile.js \
20"       | diff -uBZ testfile.js -
21
22let b:undo_indent = 'setlocal indentexpr< smartindent< autoindent< indentkeys<'
23
24" Only define the function once.
25if exists('*GetJavascriptIndent')
26  finish
27endif
28
29let s:cpo_save = &cpo
30set cpo&vim
31
32" indent correctly if inside <script>
33" vim/vim@690afe1 for the switch from cindent
34" overridden with b:html_indent_script1
35call extend(g:,{'html_indent_script1': 'inc'},'keep')
36
37" Regex of syntax group names that are or delimit string or are comments.
38let s:bvars = {
39      \ 'syng_strcom': 'string\|comment\|regex\|special\|doc\|template\%(braces\)\@!',
40      \ 'syng_str': 'string\|template\|special' }
41" template strings may want to be excluded when editing graphql:
42" au! Filetype javascript let b:syng_str = '^\%(.*template\)\@!.*string\|special'
43" au! Filetype javascript let b:syng_strcom = '^\%(.*template\)\@!.*string\|comment\|regex\|special\|doc'
44
45function s:GetVars()
46  call extend(b:,extend(s:bvars,{'js_cache': [0,0,0]}),'keep')
47endfunction
48
49" Get shiftwidth value
50if exists('*shiftwidth')
51  function s:sw()
52    return shiftwidth()
53  endfunction
54else
55  function s:sw()
56    return &l:shiftwidth ? &l:shiftwidth : &l:tabstop
57  endfunction
58endif
59
60" Performance for forwards search(): start search at pos rather than masking
61" matches before pos.
62let s:z = has('patch-7.4.984') ? 'z' : ''
63
64" Expression used to check whether we should skip a match with searchpair().
65let s:skip_expr = "s:SynAt(line('.'),col('.')) =~? b:syng_strcom"
66let s:in_comm = s:skip_expr[:-14] . "'comment\\|doc'"
67
68let s:rel = has('reltime')
69" searchpair() wrapper
70if s:rel
71  function s:GetPair(start,end,flags,skip)
72    return searchpair('\m'.a:start,'','\m'.a:end,a:flags,a:skip,s:l1,a:skip ==# 's:SkipFunc()' ? 2000 : 200)
73  endfunction
74else
75  function s:GetPair(start,end,flags,skip)
76    return searchpair('\m'.a:start,'','\m'.a:end,a:flags,a:skip,s:l1)
77  endfunction
78endif
79
80function s:SynAt(l,c)
81  let byte = line2byte(a:l) + a:c - 1
82  let pos = index(s:synid_cache[0], byte)
83  if pos == -1
84    let s:synid_cache[:] += [[byte], [synIDattr(synID(a:l, a:c, 0), 'name')]]
85  endif
86  return s:synid_cache[1][pos]
87endfunction
88
89function s:ParseCino(f)
90  let [divider, n, cstr] = [0] + matchlist(&cino,
91        \ '\%(.*,\)\=\%(\%d'.char2nr(a:f).'\(-\)\=\([.s0-9]*\)\)\=')[1:2]
92  for c in split(cstr,'\zs')
93    if c == '.' && !divider
94      let divider = 1
95    elseif c ==# 's'
96      if n !~ '\d'
97        return n . s:sw() + 0
98      endif
99      let n = str2nr(n) * s:sw()
100      break
101    else
102      let [n, divider] .= [c, 0]
103    endif
104  endfor
105  return str2nr(n) / max([str2nr(divider),1])
106endfunction
107
108" Optimized {skip} expr, only callable from the search loop which
109" GetJavascriptIndent does to find the containing [[{(] (side-effects)
110function s:SkipFunc()
111  if s:top_col == 1
112    throw 'out of bounds'
113  elseif s:check_in
114    if eval(s:skip_expr)
115      return 1
116    endif
117    let s:check_in = 0
118  elseif getline('.') =~ '\%<'.col('.').'c\/.\{-}\/\|\%>'.col('.').'c[''"]\|\\$'
119    if eval(s:skip_expr)
120      return 1
121    endif
122  elseif search('\m`\|\${\|\*\/','nW'.s:z,s:looksyn)
123    if eval(s:skip_expr)
124      let s:check_in = 1
125      return 1
126    endif
127  else
128    let s:synid_cache[:] += [[line2byte('.') + col('.') - 1], ['']]
129  endif
130  let [s:looksyn, s:top_col] = getpos('.')[1:2]
131endfunction
132
133function s:AlternatePair()
134  let [pat, l:for] = ['[][(){};]', 2]
135  while s:SearchLoop(pat,'bW','s:SkipFunc()')
136    if s:LookingAt() == ';'
137      if !l:for
138        if s:GetPair('{','}','bW','s:SkipFunc()')
139          return
140        endif
141        break
142      else
143        let [pat, l:for] = ['[{}();]', l:for - 1]
144      endif
145    else
146      let idx = stridx('])}',s:LookingAt())
147      if idx == -1
148        return
149      elseif !s:GetPair(['\[','(','{'][idx],'])}'[idx],'bW','s:SkipFunc()')
150        break
151      endif
152    endif
153  endwhile
154  throw 'out of bounds'
155endfunction
156
157function s:Nat(int)
158  return a:int * (a:int > 0)
159endfunction
160
161function s:LookingAt()
162  return getline('.')[col('.')-1]
163endfunction
164
165function s:Token()
166  return s:LookingAt() =~ '\k' ? expand('<cword>') : s:LookingAt()
167endfunction
168
169function s:PreviousToken(...)
170  let [l:pos, tok] = [getpos('.'), '']
171  if search('\m\k\{1,}\|\S','ebW')
172    if getline('.')[col('.')-2:col('.')-1] == '*/'
173      if eval(s:in_comm) && !s:SearchLoop('\S\ze\_s*\/[/*]','bW',s:in_comm)
174        call setpos('.',l:pos)
175      else
176        let tok = s:Token()
177      endif
178    else
179      let two = a:0 || line('.') != l:pos[1] ? strridx(getline('.')[:col('.')],'//') + 1 : 0
180      if two && eval(s:in_comm)
181        call cursor(0,two)
182        let tok = s:PreviousToken(1)
183        if tok is ''
184          call setpos('.',l:pos)
185        endif
186      else
187        let tok = s:Token()
188      endif
189    endif
190  endif
191  return tok
192endfunction
193
194function s:Pure(f,...)
195  return eval("[call(a:f,a:000),cursor(a:firstline,".col('.').")][0]")
196endfunction
197
198function s:SearchLoop(pat,flags,expr)
199  return s:GetPair(a:pat,'\_$.',a:flags,a:expr)
200endfunction
201
202function s:ExprCol()
203  if getline('.')[col('.')-2] == ':'
204    return 1
205  endif
206  let bal = 0
207  while s:SearchLoop('[{}?:]','bW',s:skip_expr)
208    if s:LookingAt() == ':'
209      if getline('.')[col('.')-2] == ':'
210        call cursor(0,col('.')-1)
211        continue
212      endif
213      let bal -= 1
214    elseif s:LookingAt() == '?'
215      if getline('.')[col('.'):col('.')+1] =~ '^\.\d\@!'
216        continue
217      elseif !bal
218        return 1
219      endif
220      let bal += 1
221    elseif s:LookingAt() == '{'
222      return !s:IsBlock()
223    elseif !s:GetPair('{','}','bW',s:skip_expr)
224      break
225    endif
226  endwhile
227endfunction
228
229" configurable regexes that define continuation lines, not including (, {, or [.
230let s:opfirst = '^' . get(g:,'javascript_opfirst',
231      \ '\C\%([<>=,.?^%|/&]\|\([-:+]\)\1\@!\|\*\+\|!=\|in\%(stanceof\)\=\>\)')
232let s:continuation = get(g:,'javascript_continuation',
233      \ '\C\%([<=,.~!?/*^%|&:]\|+\@<!+\|-\@<!-\|=\@<!>\|\<\%(typeof\|new\|delete\|void\|in\|instanceof\|await\)\)') . '$'
234
235function s:Continues()
236  let tok = matchstr(strpart(getline('.'),col('.')-15,15),s:continuation)
237  if tok =~ '[a-z:]'
238    return tok == ':' ? s:ExprCol() : s:PreviousToken() != '.'
239  elseif tok !~ '[/>]'
240    return tok isnot ''
241  endif
242  return s:SynAt(line('.'),col('.')) !~? (tok == '>' ? 'jsflow\|^html' : 'regex')
243endfunction
244
245" Check if line 'lnum' has a balanced amount of parentheses.
246function s:Balanced(lnum,line)
247  let l:open = 0
248  let pos = match(a:line, '[][(){}]')
249  while pos != -1
250    if s:SynAt(a:lnum,pos + 1) !~? b:syng_strcom
251      let l:open += match(' ' . a:line[pos],'[[({]')
252      if l:open < 0
253        return
254      endif
255    endif
256    let pos = match(a:line, !l:open ? '[][(){}]' : '()' =~ a:line[pos] ?
257          \ '[()]' : '{}' =~ a:line[pos] ? '[{}]' : '[][]', pos + 1)
258  endwhile
259  return !l:open
260endfunction
261
262function s:OneScope()
263  if s:LookingAt() == ')' && s:GetPair('(', ')', 'bW', s:skip_expr)
264    let tok = s:PreviousToken()
265    return (count(split('for if let while with'),tok) ||
266          \ tok =~# '^await$\|^each$' && s:PreviousToken() ==# 'for') &&
267          \ s:Pure('s:PreviousToken') != '.' && !(tok == 'while' && s:DoWhile())
268  elseif s:Token() =~# '^else$\|^do$'
269    return s:Pure('s:PreviousToken') != '.'
270  elseif strpart(getline('.'),col('.')-2,2) == '=>'
271    call cursor(0,col('.')-1)
272    if s:PreviousToken() == ')'
273      return s:GetPair('(', ')', 'bW', s:skip_expr)
274    endif
275    return 1
276  endif
277endfunction
278
279function s:DoWhile()
280  let cpos = searchpos('\m\<','cbW')
281  while s:SearchLoop('\C[{}]\|\<\%(do\|while\)\>','bW',s:skip_expr)
282    if s:LookingAt() =~ '\a'
283      if s:Pure('s:IsBlock')
284        if s:LookingAt() ==# 'd'
285          return 1
286        endif
287        break
288      endif
289    elseif s:LookingAt() != '}' || !s:GetPair('{','}','bW',s:skip_expr)
290      break
291    endif
292  endwhile
293  call call('cursor',cpos)
294endfunction
295
296" returns total offset from braceless contexts. 'num' is the lineNr which
297" encloses the entire context, 'cont' if whether a:firstline is a continued
298" expression, which could have started in a braceless context
299function s:IsContOne(cont)
300  let [l:num, b_l] = [b:js_cache[1] + !b:js_cache[1], 0]
301  let pind = b:js_cache[1] ? indent(b:js_cache[1]) + s:sw() : 0
302  let ind = indent('.') + !a:cont
303  while line('.') > l:num && ind > pind || line('.') == l:num
304    if indent('.') < ind && s:OneScope()
305      let b_l += 1
306    elseif !a:cont || b_l || ind < indent(a:firstline)
307      break
308    else
309      call cursor(0,1)
310    endif
311    let ind = min([ind, indent('.')])
312    if s:PreviousToken() is ''
313      break
314    endif
315  endwhile
316  return b_l
317endfunction
318
319function s:IsSwitch()
320  call call('cursor',b:js_cache[1:])
321  return search('\m\C\%#.\_s*\%(\%(\/\/.*\_$\|\/\*\_.\{-}\*\/\)\@>\_s*\)*\%(case\|default\)\>','nWc'.s:z)
322endfunction
323
324" https://github.com/sweet-js/sweet.js/wiki/design#give-lookbehind-to-the-reader
325function s:IsBlock()
326  let tok = s:PreviousToken()
327  if join(s:stack) =~? 'xml\|jsx' && s:SynAt(line('.'),col('.')-1) =~? 'xml\|jsx'
328    let s:in_jsx = 1
329    return tok != '{'
330  elseif tok =~ '\k'
331    if tok ==# 'type'
332      return s:Pure('eval',"s:PreviousToken() !~# '^\\%(im\\|ex\\)port$' || s:PreviousToken() == '.'")
333    elseif tok ==# 'of'
334      return s:Pure('eval',"!s:GetPair('[[({]','[])}]','bW',s:skip_expr) || s:LookingAt() != '(' ||"
335            \ ."s:{s:PreviousToken() ==# 'await' ? 'Previous' : ''}Token() !=# 'for' || s:PreviousToken() == '.'")
336    endif
337    return index(split('return const let import export extends yield default delete var await void typeof throw case new in instanceof')
338          \ ,tok) < (line('.') != a:firstline) || s:Pure('s:PreviousToken') == '.'
339  elseif tok == '>'
340    return getline('.')[col('.')-2] == '=' || s:SynAt(line('.'),col('.')) =~? 'jsflow\|^html'
341  elseif tok == '*'
342    return s:Pure('s:PreviousToken') == ':'
343  elseif tok == ':'
344    return s:Pure('eval',"s:PreviousToken() =~ '^\\K\\k*$' && !s:ExprCol()")
345  elseif tok == '/'
346    return s:SynAt(line('.'),col('.')) =~? 'regex'
347  elseif tok !~ '[=~!<,.?^%|&([]'
348    return tok !~ '[-+]' || line('.') != a:firstline && getline('.')[col('.')-2] == tok
349  endif
350endfunction
351
352function GetJavascriptIndent()
353  call s:GetVars()
354  let s:synid_cache = [[],[]]
355  let l:line = getline(v:lnum)
356  " use synstack as it validates syn state and works in an empty line
357  let s:stack = [''] + map(synstack(v:lnum,1),"synIDattr(v:val,'name')")
358
359  " start with strings,comments,etc.
360  if s:stack[-1] =~? 'comment\|doc'
361    if l:line =~ '^\s*\*'
362      return cindent(v:lnum)
363    elseif l:line !~ '^\s*\/[/*]'
364      return -1
365    endif
366  elseif s:stack[-1] =~? b:syng_str
367    if b:js_cache[0] == v:lnum - 1 && s:Balanced(v:lnum-1,getline(v:lnum-1))
368      let b:js_cache[0] = v:lnum
369    endif
370    return -1
371  endif
372
373  let s:l1 = max([0,prevnonblank(v:lnum) - (s:rel ? 2000 : 1000),
374        \ get(get(b:,'hi_indent',{}),'blocklnr')])
375  call cursor(v:lnum,1)
376  if s:PreviousToken() is ''
377    return
378  endif
379  let [l:lnum, pline] = [line('.'), getline('.')[:col('.')-1]]
380
381  let l:line = substitute(l:line,'^\s*','','')
382  let l:line_raw = l:line
383  if l:line[:1] == '/*'
384    let l:line = substitute(l:line,'^\%(\/\*.\{-}\*\/\s*\)*','','')
385  endif
386  if l:line =~ '^\/[/*]'
387    let l:line = ''
388  endif
389
390  " the containing paren, bracket, or curly. Many hacks for performance
391  call cursor(v:lnum,1)
392  let idx = index([']',')','}'],l:line[0])
393  if b:js_cache[0] > l:lnum && b:js_cache[0] < v:lnum ||
394        \ b:js_cache[0] == l:lnum && s:Balanced(l:lnum,pline)
395    call call('cursor',b:js_cache[1:])
396  else
397    let [s:looksyn, s:top_col, s:check_in, s:l1] = [v:lnum - 1,0,0,
398          \ max([s:l1, &smc ? search('\m^.\{'.&smc.',}','nbW',s:l1 + 1) + 1 : 0])]
399    try
400      if idx != -1
401        call s:GetPair(['\[','(','{'][idx],'])}'[idx],'bW','s:SkipFunc()')
402      elseif getline(v:lnum) !~ '^\S' && s:stack[-1] =~? 'block\|^jsobject$'
403        call s:GetPair('{','}','bW','s:SkipFunc()')
404      else
405        call s:AlternatePair()
406      endif
407    catch /^\Cout of bounds$/
408      call cursor(v:lnum,1)
409    endtry
410    let b:js_cache[1:] = line('.') == v:lnum ? [0,0] : getpos('.')[1:2]
411  endif
412
413  let [b:js_cache[0], num] = [v:lnum, b:js_cache[1]]
414
415  let [num_ind, is_op, b_l, l:switch_offset, s:in_jsx] = [s:Nat(indent(num)),0,0,0,0]
416  if !num || s:LookingAt() == '{' && s:IsBlock()
417    let ilnum = line('.')
418    if num && !s:in_jsx && s:LookingAt() == ')' && s:GetPair('(',')','bW',s:skip_expr)
419      if ilnum == num
420        let [num, num_ind] = [line('.'), indent('.')]
421      endif
422      if idx == -1 && s:PreviousToken() ==# 'switch' && s:IsSwitch()
423        let l:switch_offset = &cino !~ ':' ? s:sw() : s:ParseCino(':')
424        if pline[-1:] != '.' && l:line =~# '^\%(default\|case\)\>'
425          return s:Nat(num_ind + l:switch_offset)
426        elseif &cino =~ '='
427          let l:case_offset = s:ParseCino('=')
428        endif
429      endif
430    endif
431    if idx == -1 && pline[-1:] !~ '[{;]'
432      call cursor(l:lnum, len(pline))
433      let sol = matchstr(l:line,s:opfirst)
434      if sol is '' || sol == '/' && s:SynAt(v:lnum,
435            \ 1 + len(getline(v:lnum)) - len(l:line)) =~? 'regex'
436        if s:Continues()
437          let is_op = s:sw()
438        endif
439      elseif num && sol =~# '^\%(in\%(stanceof\)\=\|\*\)$' &&
440            \ s:LookingAt() == '}' && s:GetPair('{','}','bW',s:skip_expr) &&
441            \ s:PreviousToken() == ')' && s:GetPair('(',')','bW',s:skip_expr) &&
442            \ (s:PreviousToken() == ']' || s:LookingAt() =~ '\k' &&
443            \ s:{s:PreviousToken() == '*' ? 'Previous' : ''}Token() !=# 'function')
444        return num_ind + s:sw()
445      else
446        let is_op = s:sw()
447      endif
448      call cursor(l:lnum, len(pline))
449      let b_l = s:Nat(s:IsContOne(is_op) - (!is_op && l:line =~ '^{')) * s:sw()
450    endif
451  elseif idx.s:LookingAt().&cino =~ '^-1(.*(' && (search('\m\S','nbW',num) || s:ParseCino('U'))
452    let pval = s:ParseCino('(')
453    if !pval
454      let [Wval, vcol] = [s:ParseCino('W'), virtcol('.')]
455      if search('\m\S','W',num)
456        return s:ParseCino('w') ? vcol : virtcol('.')-1
457      endif
458      return Wval ? s:Nat(num_ind + Wval) : vcol
459    endif
460    return s:Nat(num_ind + pval + searchpair('\m(','','\m)','nbrmW',s:skip_expr,num) * s:sw())
461  endif
462
463  " main return
464  if l:line =~ '^[])}]\|^|}'
465    if l:line_raw[0] == ')'
466      if s:ParseCino('M')
467        return indent(l:lnum)
468      elseif num && &cino =~# 'm' && !s:ParseCino('m')
469        return virtcol('.') - 1
470      endif
471    endif
472    return num_ind
473  elseif num
474    return s:Nat(num_ind + get(l:,'case_offset',s:sw()) + l:switch_offset + b_l + is_op)
475  endif
476  return b_l + is_op
477endfunction
478
479let &cpo = s:cpo_save
480unlet s:cpo_save
481