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