1" Vim indent file 2" Language: Erlang (http://www.erlang.org) 3" Author: Csaba Hoch <[email protected]> 4" Contributors: Edwin Fine <efine145_nospam01 at usa dot net> 5" Pawel 'kTT' Salata <[email protected]> 6" Ricardo Catalinas Jiménez <[email protected]> 7" Last Update: 2013-Jul-21 8" License: Vim license 9" URL: https://github.com/hcs42/vim-erlang 10 11" Note About Usage: 12" This indentation script works best with the Erlang syntax file created by 13" Kreąimir Marľić (Kresimir Marzic) and maintained by Csaba Hoch. 14 15" Notes About Implementation: 16" 17" - LTI = Line to indent. 18" - The index of the first line is 1, but the index of the first column is 0. 19 20 21" Initialization {{{1 22" ============== 23 24" Only load this indent file when no other was loaded 25" Vim 7 or later is needed 26if exists("b:did_indent") || version < 700 27 finish 28else 29 let b:did_indent = 1 30endif 31 32setlocal indentexpr=ErlangIndent() 33setlocal indentkeys+=0=end,0=of,0=catch,0=after,0=when,0=),0=],0=},0=>> 34 35" Only define the functions once 36if exists("*ErlangIndent") 37 finish 38endif 39 40let s:cpo_save = &cpo 41set cpo&vim 42 43" Logging library {{{1 44" =============== 45 46" Purpose: 47" Logs the given string using the ErlangIndentLog function if it exists. 48" Parameters: 49" s: string 50function! s:Log(s) 51 if exists("*ErlangIndentLog") 52 call ErlangIndentLog(a:s) 53 endif 54endfunction 55 56" Line tokenizer library {{{1 57" ====================== 58 59" Indtokens are "indentation tokens". 60 61" Purpose: 62" Calculate the new virtual column after the given segment of a line. 63" Parameters: 64" line: string 65" first_index: integer -- the index of the first character of the segment 66" last_index: integer -- the index of the last character of the segment 67" vcol: integer -- the virtual column of the first character of the token 68" tabstop: integer -- the value of the 'tabstop' option to be used 69" Returns: 70" vcol: integer 71" Example: 72" " index: 0 12 34567 73" " vcol: 0 45 89 74" s:CalcVCol("\t'\tx', b", 1, 4, 4) -> 10 75function! s:CalcVCol(line, first_index, last_index, vcol, tabstop) 76 77 " We copy the relevent segment of the line, otherwise if the line were 78 " e.g. `"\t", term` then the else branch below would consume the `", term` 79 " part at once. 80 let line = a:line[a:first_index : a:last_index] 81 82 let i = 0 83 let last_index = a:last_index - a:first_index 84 let vcol = a:vcol 85 86 while 0 <= i && i <= last_index 87 88 if line[i] ==# "\t" 89 " Example (when tabstop == 4): 90 " 91 " vcol + tab -> next_vcol 92 " 0 + tab -> 4 93 " 1 + tab -> 4 94 " 2 + tab -> 4 95 " 3 + tab -> 4 96 " 4 + tab -> 8 97 " 98 " next_i - i == the number of tabs 99 let next_i = matchend(line, '\t*', i + 1) 100 let vcol = (vcol / a:tabstop + (next_i - i)) * a:tabstop 101 call s:Log('new vcol after tab: '. vcol) 102 else 103 let next_i = matchend(line, '[^\t]*', i + 1) 104 let vcol += next_i - i 105 call s:Log('new vcol after other: '. vcol) 106 endif 107 let i = next_i 108 endwhile 109 110 return vcol 111endfunction 112 113" Purpose: 114" Go through the whole line and return the tokens in the line. 115" Parameters: 116" line: string -- the line to be examined 117" string_continuation: bool 118" atom_continuation: bool 119" Returns: 120" indtokens = [indtoken] 121" indtoken = [token, vcol, col] 122" token = string (examples: 'begin', '<variable>', '}') 123" vcol = integer (the virtual column of the first character of the token) 124" col = integer 125function! s:GetTokensFromLine(line, string_continuation, atom_continuation, 126 \tabstop) 127 128 let linelen = strlen(a:line) " The length of the line 129 let i = 0 " The index of the current character in the line 130 let vcol = 0 " The virtual column of the current character 131 let indtokens = [] 132 133 if a:string_continuation 134 let i = matchend(a:line, '^\%([^"\\]\|\\.\)*"', 0) 135 if i ==# -1 136 call s:Log(' Whole line is string continuation -> ignore') 137 return [] 138 else 139 let vcol = s:CalcVCol(a:line, 0, i - 1, 0, a:tabstop) 140 call add(indtokens, ['<string_end>', vcol, i]) 141 endif 142 elseif a:atom_continuation 143 let i = matchend(a:line, "^\\%([^'\\\\]\\|\\\\.\\)*'", 0) 144 if i ==# -1 145 call s:Log(' Whole line is quoted atom continuation -> ignore') 146 return [] 147 else 148 let vcol = s:CalcVCol(a:line, 0, i - 1, 0, a:tabstop) 149 call add(indtokens, ['<quoted_atom_end>', vcol, i]) 150 endif 151 endif 152 153 while 0 <= i && i < linelen 154 155 let next_vcol = '' 156 157 " Spaces 158 if a:line[i] ==# ' ' 159 let next_i = matchend(a:line, ' *', i + 1) 160 161 " Tabs 162 elseif a:line[i] ==# "\t" 163 let next_i = matchend(a:line, '\t*', i + 1) 164 165 " See example in s:CalcVCol 166 let next_vcol = (vcol / a:tabstop + (next_i - i)) * a:tabstop 167 168 " Comment 169 elseif a:line[i] ==# '%' 170 let next_i = linelen 171 172 " String token: "..." 173 elseif a:line[i] ==# '"' 174 let next_i = matchend(a:line, '\%([^"\\]\|\\.\)*"', i + 1) 175 if next_i ==# -1 176 call add(indtokens, ['<string_start>', vcol, i]) 177 else 178 let next_vcol = s:CalcVCol(a:line, i, next_i - 1, vcol, a:tabstop) 179 call add(indtokens, ['<string>', vcol, i]) 180 endif 181 182 " Quoted atom token: '...' 183 elseif a:line[i] ==# "'" 184 let next_i = matchend(a:line, "\\%([^'\\\\]\\|\\\\.\\)*'", i + 1) 185 if next_i ==# -1 186 call add(indtokens, ['<quoted_atom_start>', vcol, i]) 187 else 188 let next_vcol = s:CalcVCol(a:line, i, next_i - 1, vcol, a:tabstop) 189 call add(indtokens, ['<quoted_atom>', vcol, i]) 190 endif 191 192 " Keyword or atom or variable token or number 193 elseif a:line[i] =~# '[a-zA-Z_@0-9]' 194 let next_i = matchend(a:line, 195 \'[[:alnum:]_@:]*\%(\s*#\s*[[:alnum:]_@:]*\)\=', 196 \i + 1) 197 call add(indtokens, [a:line[(i):(next_i - 1)], vcol, i]) 198 199 " Character token: $<char> (as in: $a) 200 elseif a:line[i] ==# '$' 201 call add(indtokens, ['$.', vcol, i]) 202 let next_i = i + 2 203 204 " Dot token: . 205 elseif a:line[i] ==# '.' 206 207 let next_i = i + 1 208 209 if i + 1 ==# linelen || a:line[i + 1] =~# '[[:blank:]%]' 210 " End of clause token: . (as in: f() -> ok.) 211 call add(indtokens, ['<end_of_clause>', vcol, i]) 212 213 else 214 " Possibilities: 215 " - Dot token in float: . (as in: 3.14) 216 " - Dot token in record: . (as in: #myrec.myfield) 217 call add(indtokens, ['.', vcol, i]) 218 endif 219 220 " Equal sign 221 elseif a:line[i] ==# '=' 222 " This is handled separately so that "=<<" will be parsed as 223 " ['=', '<<'] instead of ['=<', '<']. Although Erlang parses it 224 " currently in the latter way, that may be fixed some day. 225 call add(indtokens, [a:line[i], vcol, i]) 226 let next_i = i + 1 227 228 " Three-character tokens 229 elseif i + 1 < linelen && 230 \ index(['=:=', '=/='], a:line[i : i + 1]) != -1 231 call add(indtokens, [a:line[i : i + 1], vcol, i]) 232 let next_i = i + 2 233 234 " Two-character tokens 235 elseif i + 1 < linelen && 236 \ index(['->', '<<', '>>', '||', '==', '/=', '=<', '>=', '++', '--', 237 \ '::'], 238 \ a:line[i : i + 1]) != -1 239 call add(indtokens, [a:line[i : i + 1], vcol, i]) 240 let next_i = i + 2 241 242 " Other character: , ; < > ( ) [ ] { } # + - * / : ? = ! | 243 else 244 call add(indtokens, [a:line[i], vcol, i]) 245 let next_i = i + 1 246 247 endif 248 249 if next_vcol ==# '' 250 let vcol += next_i - i 251 else 252 let vcol = next_vcol 253 endif 254 255 let i = next_i 256 257 endwhile 258 259 return indtokens 260 261endfunction 262 263" TODO: doc, handle "not found" case 264function! s:GetIndtokenAtCol(indtokens, col) 265 let i = 0 266 while i < len(a:indtokens) 267 if a:indtokens[i][2] ==# a:col 268 return [1, i] 269 elseif a:indtokens[i][2] > a:col 270 return [0, s:IndentError('No token at col ' . a:col . ', ' . 271 \'indtokens = ' . string(a:indtokens), 272 \'', '')] 273 endif 274 let i += 1 275 endwhile 276 return [0, s:IndentError('No token at col ' . a:col . ', ' . 277 \'indtokens = ' . string(a:indtokens), 278 \'', '')] 279endfunction 280 281" Stack library {{{1 282" ============= 283 284" Purpose: 285" Push a token onto the parser's stack. 286" Parameters: 287" stack: [token] 288" token: string 289function! s:Push(stack, token) 290 call s:Log(' Stack Push: "' . a:token . '" into ' . string(a:stack)) 291 call insert(a:stack, a:token) 292endfunction 293 294" Purpose: 295" Pop a token from the parser's stack. 296" Parameters: 297" stack: [token] 298" token: string 299" Returns: 300" token: string -- the removed element 301function! s:Pop(stack) 302 let head = remove(a:stack, 0) 303 call s:Log(' Stack Pop: "' . head . '" from ' . string(a:stack)) 304 return head 305endfunction 306 307" Library for accessing and storing tokenized lines {{{1 308" ================================================= 309 310" The Erlang token cache: an `lnum -> indtokens` dictionary that stores the 311" tokenized lines. 312let s:all_tokens = {} 313let s:file_name = '' 314let s:last_changedtick = -1 315 316" Purpose: 317" Clear the Erlang token cache if we have a different file or the file has 318" been changed since the last indentation. 319function! s:ClearTokenCacheIfNeeded() 320 let file_name = expand('%:p') 321 if file_name != s:file_name || 322 \ b:changedtick != s:last_changedtick 323 let s:file_name = file_name 324 let s:last_changedtick = b:changedtick 325 let s:all_tokens = {} 326 endif 327endfunction 328 329" Purpose: 330" Return the tokens of line `lnum`, if that line is not empty. If it is 331" empty, find the first non-empty line in the given `direction` and return 332" the tokens of that line. 333" Parameters: 334" lnum: integer 335" direction: 'up' | 'down' 336" Returns: 337" result: [] -- the result is an empty list if we hit the beginning or end 338" of the file 339" | [lnum, indtokens] 340" lnum: integer -- the index of the non-empty line that was found and 341" tokenized 342" indtokens: [indtoken] -- the tokens of line `lnum` 343function! s:TokenizeLine(lnum, direction) 344 345 call s:Log('Tokenizing starts from line ' . a:lnum) 346 if a:direction ==# 'up' 347 let lnum = prevnonblank(a:lnum) 348 else " a:direction ==# 'down' 349 let lnum = nextnonblank(a:lnum) 350 endif 351 352 " We hit the beginning or end of the file 353 if lnum ==# 0 354 let indtokens = [] 355 call s:Log(' We hit the beginning or end of the file.') 356 357 " The line has already been parsed 358 elseif has_key(s:all_tokens, lnum) 359 let indtokens = s:all_tokens[lnum] 360 call s:Log('Cached line ' . lnum . ': ' . getline(lnum)) 361 call s:Log(" Tokens in the line:\n - " . join(indtokens, "\n - ")) 362 363 " The line should be parsed now 364 else 365 366 " Parse the line 367 let line = getline(lnum) 368 let string_continuation = s:IsLineStringContinuation(lnum) 369 let atom_continuation = s:IsLineAtomContinuation(lnum) 370 let indtokens = s:GetTokensFromLine(line, string_continuation, 371 \atom_continuation, &tabstop) 372 let s:all_tokens[lnum] = indtokens 373 call s:Log('Tokenizing line ' . lnum . ': ' . line) 374 call s:Log(" Tokens in the line:\n - " . join(indtokens, "\n - ")) 375 376 endif 377 378 return [lnum, indtokens] 379endfunction 380 381" Purpose: 382" As a helper function for PrevIndToken and NextIndToken, the FindIndToken 383" function finds the first line with at least one token in the given 384" direction. 385" Parameters: 386" lnum: integer 387" direction: 'up' | 'down' 388" Returns: 389" result: [] -- the result is an empty list if we hit the beginning or end 390" of the file 391" | indtoken 392function! s:FindIndToken(lnum, dir) 393 let lnum = a:lnum 394 while 1 395 let lnum += (a:dir ==# 'up' ? -1 : 1) 396 let [lnum, indtokens] = s:TokenizeLine(lnum, a:dir) 397 if lnum ==# 0 398 " We hit the beginning or end of the file 399 return [] 400 elseif !empty(indtokens) 401 return indtokens[a:dir ==# 'up' ? -1 : 0] 402 endif 403 endwhile 404endfunction 405 406" Purpose: 407" Find the token that directly precedes the given token. 408" Parameters: 409" lnum: integer -- the line of the given token 410" i: the index of the given token within line `lnum` 411" Returns: 412" result = [] -- the result is an empty list if the given token is the first 413" token of the file 414" | indtoken 415function! s:PrevIndToken(lnum, i) 416 call s:Log(' PrevIndToken called: lnum=' . a:lnum . ', i =' . a:i) 417 418 " If the current line has a previous token, return that 419 if a:i > 0 420 return s:all_tokens[a:lnum][a:i - 1] 421 else 422 return s:FindIndToken(a:lnum, 'up') 423 endif 424endfunction 425 426" Purpose: 427" Find the token that directly succeeds the given token. 428" Parameters: 429" lnum: integer -- the line of the given token 430" i: the index of the given token within line `lnum` 431" Returns: 432" result = [] -- the result is an empty list if the given token is the last 433" token of the file 434" | indtoken 435function! s:NextIndToken(lnum, i) 436 call s:Log(' NextIndToken called: lnum=' . a:lnum . ', i =' . a:i) 437 438 " If the current line has a next token, return that 439 if len(s:all_tokens[a:lnum]) > a:i + 1 440 return s:all_tokens[a:lnum][a:i + 1] 441 else 442 return s:FindIndToken(a:lnum, 'down') 443 endif 444endfunction 445 446" ErlangCalcIndent helper functions {{{1 447" ================================= 448 449" Purpose: 450" This function is called when the parser encounters a syntax error. 451" 452" If we encounter a syntax error, we return 453" g:erlang_unexpected_token_indent, which is -1 by default. This means that 454" the indentation of the LTI will not be changed. 455" Parameter: 456" msg: string 457" token: string 458" stack: [token] 459" Returns: 460" indent: integer 461function! s:IndentError(msg, token, stack) 462 call s:Log('Indent error: ' . a:msg . ' -> return') 463 call s:Log(' Token = ' . a:token . ', ' . 464 \' stack = ' . string(a:stack)) 465 return g:erlang_unexpected_token_indent 466endfunction 467 468" Purpose: 469" This function is called when the parser encounters an unexpected token, 470" and the parser will return the number given back by UnexpectedToken. 471" 472" If we encounter an unexpected token, we return 473" g:erlang_unexpected_token_indent, which is -1 by default. This means that 474" the indentation of the LTI will not be changed. 475" Parameter: 476" token: string 477" stack: [token] 478" Returns: 479" indent: integer 480function! s:UnexpectedToken(token, stack) 481 call s:Log(' Unexpected token ' . a:token . ', stack = ' . 482 \string(a:stack) . ' -> return') 483 return g:erlang_unexpected_token_indent 484endfunction 485 486if !exists('g:erlang_unexpected_token_indent') 487 let g:erlang_unexpected_token_indent = -1 488endif 489 490" Purpose: 491" Return whether the given line starts with a string continuation. 492" Parameter: 493" lnum: integer 494" Returns: 495" result: bool 496" Example: 497" f() -> % IsLineStringContinuation = false 498" "This is a % IsLineStringContinuation = false 499" multiline % IsLineStringContinuation = true 500" string". % IsLineStringContinuation = true 501function! s:IsLineStringContinuation(lnum) 502 if has('syntax_items') 503 return synIDattr(synID(a:lnum, 1, 0), 'name') =~# '^erlangString' 504 else 505 return 0 506 endif 507endfunction 508 509" Purpose: 510" Return whether the given line starts with an atom continuation. 511" Parameter: 512" lnum: integer 513" Returns: 514" result: bool 515" Example: 516" 'function with % IsLineAtomContinuation = true, but should be false 517" weird name'() -> % IsLineAtomContinuation = true 518" ok. % IsLineAtomContinuation = false 519function! s:IsLineAtomContinuation(lnum) 520 if has('syntax_items') 521 return synIDattr(synID(a:lnum, 1, 0), 'name') =~# '^erlangQuotedAtom' 522 else 523 return 0 524 endif 525endfunction 526 527" Purpose: 528" Return whether the 'catch' token (which should be the `i`th token in line 529" `lnum`) is standalone or part of a try-catch block, based on the preceding 530" token. 531" Parameters: 532" lnum: integer 533" i: integer 534" Return: 535" is_standalone: bool 536function! s:IsCatchStandalone(lnum, i) 537 call s:Log(' IsCatchStandalone called: lnum=' . a:lnum . ', i=' . a:i) 538 let prev_indtoken = s:PrevIndToken(a:lnum, a:i) 539 540 " If we hit the beginning of the file, it is not a catch in a try block 541 if prev_indtoken == [] 542 return 1 543 endif 544 545 let prev_token = prev_indtoken[0] 546 547 if prev_token =~# '[A-Z_@0-9]' 548 let is_standalone = 0 549 elseif prev_token =~# '[a-z]' 550 if index(['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 551 \ 'bsr', 'bxor', 'case', 'catch', 'div', 'not', 'or', 'orelse', 552 \ 'rem', 'try', 'xor'], prev_token) != -1 553 " If catch is after these keywords, it is standalone 554 let is_standalone = 1 555 else 556 " If catch is after another keyword (e.g. 'end') or an atom, it is 557 " part of try-catch. 558 " 559 " Keywords: 560 " - may precede 'catch': end 561 " - may not precede 'catch': fun if of receive when 562 " - unused: cond let query 563 let is_standalone = 0 564 endif 565 elseif index([')', ']', '}', '<string>', '<string_end>', '<quoted_atom>', 566 \ '<quoted_atom_end>', '$.'], prev_token) != -1 567 let is_standalone = 0 568 else 569 " This 'else' branch includes the following tokens: 570 " -> == /= =< < >= > =:= =/= + - * / ++ -- :: < > ; ( [ { ? = ! . | 571 let is_standalone = 1 572 endif 573 574 call s:Log(' "catch" preceded by "' . prev_token . '" -> catch ' . 575 \(is_standalone ? 'is standalone' : 'belongs to try-catch')) 576 return is_standalone 577 578endfunction 579 580" Purpose: 581" This function is called when a begin-type element ('begin', 'case', 582" '[', '<<', etc.) is found. It asks the caller to return if the stack 583" Parameters: 584" stack: [token] 585" token: string 586" curr_vcol: integer 587" stored_vcol: integer 588" sw: integer -- number of spaces to be used after the begin element as 589" indentation 590" Returns: 591" result: [should_return, indent] 592" should_return: bool -- if true, the caller should return `indent` to Vim 593" indent -- integer 594function! s:BeginElementFoundIfEmpty(stack, token, curr_vcol, stored_vcol, sw) 595 if empty(a:stack) 596 if a:stored_vcol ==# -1 597 call s:Log(' "' . a:token . '" directly preceeds LTI -> return') 598 return [1, a:curr_vcol + a:sw] 599 else 600 call s:Log(' "' . a:token . 601 \'" token (whose expression includes LTI) found -> return') 602 return [1, a:stored_vcol] 603 endif 604 else 605 return [0, 0] 606 endif 607endfunction 608 609" Purpose: 610" This function is called when a begin-type element ('begin', 'case', '[', 611" '<<', etc.) is found, and in some cases when 'after' and 'when' is found. 612" It asks the caller to return if the stack is already empty. 613" Parameters: 614" stack: [token] 615" token: string 616" curr_vcol: integer 617" stored_vcol: integer 618" end_token: end token that belongs to the begin element found (e.g. if the 619" begin element is 'begin', the end token is 'end') 620" sw: integer -- number of spaces to be used after the begin element as 621" indentation 622" Returns: 623" result: [should_return, indent] 624" should_return: bool -- if true, the caller should return `indent` to Vim 625" indent -- integer 626function! s:BeginElementFound(stack, token, curr_vcol, stored_vcol, end_token, sw) 627 628 " Return 'return' if the stack is empty 629 let [ret, res] = s:BeginElementFoundIfEmpty(a:stack, a:token, a:curr_vcol, 630 \a:stored_vcol, a:sw) 631 if ret | return [ret, res] | endif 632 633 if a:stack[0] ==# a:end_token 634 call s:Log(' "' . a:token . '" pops "' . a:end_token . '"') 635 call s:Pop(a:stack) 636 if !empty(a:stack) && a:stack[0] ==# 'align_to_begin_element' 637 call s:Pop(a:stack) 638 if empty(a:stack) 639 return [1, a:curr_vcol] 640 else 641 return [1, s:UnexpectedToken(a:token, a:stack)] 642 endif 643 else 644 return [0, 0] 645 endif 646 else 647 return [1, s:UnexpectedToken(a:token, a:stack)] 648 endif 649endfunction 650 651" Purpose: 652" This function is called when we hit the beginning of a file or an 653" end-of-clause token -- i.e. when we found the beginning of the current 654" clause. 655" 656" If the stack contains an '->' or 'when', this means that we can return 657" now, since we were looking for the beginning of the clause. 658" Parameters: 659" stack: [token] 660" token: string 661" stored_vcol: integer 662" Returns: 663" result: [should_return, indent] 664" should_return: bool -- if true, the caller should return `indent` to Vim 665" indent -- integer 666function! s:BeginningOfClauseFound(stack, token, stored_vcol) 667 if !empty(a:stack) && a:stack[0] ==# 'when' 668 call s:Log(' BeginningOfClauseFound: "when" found in stack') 669 call s:Pop(a:stack) 670 if empty(a:stack) 671 call s:Log(' Stack is ["when"], so LTI is in a guard -> return') 672 return [1, a:stored_vcol + &sw + 2] 673 else 674 return [1, s:UnexpectedToken(a:token, a:stack)] 675 endif 676 elseif !empty(a:stack) && a:stack[0] ==# '->' 677 call s:Log(' BeginningOfClauseFound: "->" found in stack') 678 call s:Pop(a:stack) 679 if empty(a:stack) 680 call s:Log(' Stack is ["->"], so LTI is in function body -> return') 681 return [1, a:stored_vcol + &sw] 682 elseif a:stack[0] ==# ';' 683 call s:Pop(a:stack) 684 if empty(a:stack) 685 call s:Log(' Stack is ["->", ";"], so LTI is in a function head ' . 686 \'-> return') 687 return [0, a:stored_vcol] 688 else 689 return [1, s:UnexpectedToken(a:token, a:stack)] 690 endif 691 else 692 return [1, s:UnexpectedToken(a:token, a:stack)] 693 endif 694 else 695 return [0, 0] 696 endif 697endfunction 698 699let g:erlang_indent_searchpair_timeout = 2000 700 701" TODO 702function! s:SearchPair(lnum, curr_col, start, middle, end) 703 call cursor(a:lnum, a:curr_col + 1) 704 let [lnum_new, col1_new] = 705 \searchpairpos(a:start, a:middle, a:end, 'bW', 706 \'synIDattr(synID(line("."), col("."), 0), "name") ' . 707 \'=~? "string\\|quotedatom\\|todo\\|comment\\|' . 708 \'erlangmodifier"', 709 \0, g:erlang_indent_searchpair_timeout) 710 return [lnum_new, col1_new - 1] 711endfunction 712 713function! s:SearchEndPair(lnum, curr_col) 714 return s:SearchPair( 715 \ a:lnum, a:curr_col, 716 \ '\C\<\%(case\|try\|begin\|receive\|if\)\>\|' . 717 \ '\<fun\>\%(\s\|\n\|%.*$\)*(', 718 \ '', 719 \ '\<end\>') 720endfunction 721 722" ErlangCalcIndent {{{1 723" ================ 724 725" Purpose: 726" Calculate the indentation of the given line. 727" Parameters: 728" lnum: integer -- index of the line for which the indentation should be 729" calculated 730" stack: [token] -- initial stack 731" Return: 732" indent: integer -- if -1, that means "don't change the indentation"; 733" otherwise it means "indent the line with `indent` 734" number of spaces or equivalent tabs" 735function! s:ErlangCalcIndent(lnum, stack) 736 let res = s:ErlangCalcIndent2(a:lnum, a:stack) 737 call s:Log("ErlangCalcIndent returned: " . res) 738 return res 739endfunction 740 741function! s:ErlangCalcIndent2(lnum, stack) 742 743 let lnum = a:lnum 744 let stored_vcol = -1 " Virtual column of the first character of the token that 745 " we currently think we might align to. 746 let mode = 'normal' 747 let stack = a:stack 748 let semicolon_abscol = '' 749 750 " Walk through the lines of the buffer backwards (starting from the 751 " previous line) until we can decide how to indent the current line. 752 while 1 753 754 let [lnum, indtokens] = s:TokenizeLine(lnum, 'up') 755 756 " Hit the start of the file 757 if lnum ==# 0 758 let [ret, res] = s:BeginningOfClauseFound(stack, 'beginning_of_file', 759 \stored_vcol) 760 if ret | return res | endif 761 762 return 0 763 endif 764 765 let i = len(indtokens) - 1 766 let last_token_of_line = 1 767 768 while i >= 0 769 770 let [token, curr_vcol, curr_col] = indtokens[i] 771 call s:Log(' Analyzing the following token: ' . string(indtokens[i])) 772 773 if len(stack) > 256 " TODO: magic number 774 return s:IndentError('Stack too long', token, stack) 775 endif 776 777 if token ==# '<end_of_clause>' 778 let [ret, res] = s:BeginningOfClauseFound(stack, token, stored_vcol) 779 if ret | return res | endif 780 781 if stored_vcol ==# -1 782 call s:Log(' End of clause directly preceeds LTI -> return') 783 return 0 784 else 785 call s:Log(' End of clause (but not end of line) -> return') 786 return stored_vcol 787 endif 788 789 elseif stack == ['prev_term_plus'] 790 if token =~# '[a-zA-Z_@]' || 791 \ token ==# '<string>' || token ==# '<string_start>' || 792 \ token ==# '<quoted_atom>' || token ==# '<quoted_atom_start>' 793 call s:Log(' previous token found: curr_vcol + plus = ' . 794 \curr_vcol . " + " . plus) 795 return curr_vcol + plus 796 endif 797 798 elseif token ==# 'begin' 799 let [ret, res] = s:BeginElementFound(stack, token, curr_vcol, 800 \stored_vcol, 'end', &sw) 801 if ret | return res | endif 802 803 " case EXPR of BRANCHES end 804 " try EXPR catch BRANCHES end 805 " try EXPR after BODY end 806 " try EXPR catch BRANCHES after BODY end 807 " try EXPR of BRANCHES catch BRANCHES end 808 " try EXPR of BRANCHES after BODY end 809 " try EXPR of BRANCHES catch BRANCHES after BODY end 810 " receive BRANCHES end 811 " receive BRANCHES after BRANCHES end 812 813 " This branch is not Emacs-compatible 814 elseif (index(['of', 'receive', 'after', 'if'], token) != -1 || 815 \ (token ==# 'catch' && !s:IsCatchStandalone(lnum, i))) && 816 \ !last_token_of_line && 817 \ (empty(stack) || stack ==# ['when'] || stack ==# ['->'] || 818 \ stack ==# ['->', ';']) 819 820 " If we are after of/receive, but these are not the last 821 " tokens of the line, we want to indent like this: 822 " 823 " % stack == [] 824 " receive stored_vcol, 825 " LTI 826 " 827 " % stack == ['->', ';'] 828 " receive stored_vcol -> 829 " B; 830 " LTI 831 " 832 " % stack == ['->'] 833 " receive stored_vcol -> 834 " LTI 835 " 836 " % stack == ['when'] 837 " receive stored_vcol when 838 " LTI 839 840 " stack = [] => LTI is a condition 841 " stack = ['->'] => LTI is a branch 842 " stack = ['->', ';'] => LTI is a condition 843 " stack = ['when'] => LTI is a guard 844 if empty(stack) || stack == ['->', ';'] 845 call s:Log(' LTI is in a condition after ' . 846 \'"of/receive/after/if/catch" -> return') 847 return stored_vcol 848 elseif stack == ['->'] 849 call s:Log(' LTI is in a branch after ' . 850 \'"of/receive/after/if/catch" -> return') 851 return stored_vcol + &sw 852 elseif stack == ['when'] 853 call s:Log(' LTI is in a guard after ' . 854 \'"of/receive/after/if/catch" -> return') 855 return stored_vcol + &sw 856 else 857 return s:UnexpectedToken(token, stack) 858 endif 859 860 elseif index(['case', 'if', 'try', 'receive'], token) != -1 861 862 " stack = [] => LTI is a condition 863 " stack = ['->'] => LTI is a branch 864 " stack = ['->', ';'] => LTI is a condition 865 " stack = ['when'] => LTI is in a guard 866 if empty(stack) 867 " pass 868 elseif (token ==# 'case' && stack[0] ==# 'of') || 869 \ (token ==# 'if') || 870 \ (token ==# 'try' && (stack[0] ==# 'of' || 871 \ stack[0] ==# 'catch' || 872 \ stack[0] ==# 'after')) || 873 \ (token ==# 'receive') 874 875 " From the indentation point of view, the keyword 876 " (of/catch/after/end) before the LTI is what counts, so 877 " when we reached these tokens, and the stack already had 878 " a catch/after/end, we didn't modify it. 879 " 880 " This way when we reach case/try/receive (i.e. now), 881 " there is at most one of/catch/after/end token in the 882 " stack. 883 if token ==# 'case' || token ==# 'try' || 884 \ (token ==# 'receive' && stack[0] ==# 'after') 885 call s:Pop(stack) 886 endif 887 888 if empty(stack) 889 call s:Log(' LTI is in a condition; matching ' . 890 \'"case/if/try/receive" found') 891 let stored_vcol = curr_vcol + &sw 892 elseif stack[0] ==# 'align_to_begin_element' 893 call s:Pop(stack) 894 let stored_vcol = curr_vcol 895 elseif len(stack) > 1 && stack[0] ==# '->' && stack[1] ==# ';' 896 call s:Log(' LTI is in a condition; matching ' . 897 \'"case/if/try/receive" found') 898 call s:Pop(stack) 899 call s:Pop(stack) 900 let stored_vcol = curr_vcol + &sw 901 elseif stack[0] ==# '->' 902 call s:Log(' LTI is in a branch; matching ' . 903 \'"case/if/try/receive" found') 904 call s:Pop(stack) 905 let stored_vcol = curr_vcol + 2 * &sw 906 elseif stack[0] ==# 'when' 907 call s:Log(' LTI is in a guard; matching ' . 908 \'"case/if/try/receive" found') 909 call s:Pop(stack) 910 let stored_vcol = curr_vcol + 2 * &sw + 2 911 endif 912 913 endif 914 915 let [ret, res] = s:BeginElementFound(stack, token, curr_vcol, 916 \stored_vcol, 'end', &sw) 917 if ret | return res | endif 918 919 elseif token ==# 'fun' 920 let next_indtoken = s:NextIndToken(lnum, i) 921 call s:Log(' Next indtoken = ' . string(next_indtoken)) 922 923 if !empty(next_indtoken) && next_indtoken[0] ==# '(' 924 " We have an anonymous function definition 925 " (e.g. "fun () -> ok end") 926 927 " stack = [] => LTI is a condition 928 " stack = ['->'] => LTI is a branch 929 " stack = ['->', ';'] => LTI is a condition 930 " stack = ['when'] => LTI is in a guard 931 if empty(stack) 932 call s:Log(' LTI is in a condition; matching "fun" found') 933 let stored_vcol = curr_vcol + &sw 934 elseif len(stack) > 1 && stack[0] ==# '->' && stack[1] ==# ';' 935 call s:Log(' LTI is in a condition; matching "fun" found') 936 call s:Pop(stack) 937 call s:Pop(stack) 938 elseif stack[0] ==# '->' 939 call s:Log(' LTI is in a branch; matching "fun" found') 940 call s:Pop(stack) 941 let stored_vcol = curr_vcol + 2 * &sw 942 elseif stack[0] ==# 'when' 943 call s:Log(' LTI is in a guard; matching "fun" found') 944 call s:Pop(stack) 945 let stored_vcol = curr_vcol + 2 * &sw + 2 946 endif 947 948 let [ret, res] = s:BeginElementFound(stack, token, curr_vcol, 949 \stored_vcol, 'end', &sw) 950 if ret | return res | endif 951 else 952 " Pass: we have a function reference (e.g. "fun f/0") 953 endif 954 955 elseif token ==# '[' 956 " Emacs compatibility 957 let [ret, res] = s:BeginElementFound(stack, token, curr_vcol, 958 \stored_vcol, ']', 1) 959 if ret | return res | endif 960 961 elseif token ==# '<<' 962 " Emacs compatibility 963 let [ret, res] = s:BeginElementFound(stack, token, curr_vcol, 964 \stored_vcol, '>>', 2) 965 if ret | return res | endif 966 967 elseif token ==# '(' || token ==# '{' 968 969 let end_token = (token ==# '(' ? ')' : 970 \token ==# '{' ? '}' : 'error') 971 972 if empty(stack) 973 " We found the opening paren whose block contains the LTI. 974 let mode = 'inside' 975 elseif stack[0] ==# end_token 976 call s:Log(' "' . token . '" pops "' . end_token . '"') 977 call s:Pop(stack) 978 979 if !empty(stack) && stack[0] ==# 'align_to_begin_element' 980 " We found the opening paren whose closing paren 981 " starts LTI 982 let mode = 'align_to_begin_element' 983 else 984 " We found the opening pair for a closing paren that 985 " was already in the stack. 986 let mode = 'outside' 987 endif 988 else 989 return s:UnexpectedToken(token, stack) 990 endif 991 992 if mode ==# 'inside' || mode ==# 'align_to_begin_element' 993 994 if last_token_of_line && i != 0 995 " Examples: {{{ 996 " 997 " mode == 'inside': 998 " 999 " my_func( 1000 " LTI 1001 " 1002 " [Variable, { 1003 " LTI 1004 " 1005 " mode == 'align_to_begin_element': 1006 " 1007 " my_func( 1008 " Params 1009 " ) % LTI 1010 " 1011 " [Variable, { 1012 " Terms 1013 " } % LTI 1014 " }}} 1015 let stack = ['prev_term_plus'] 1016 let plus = (mode ==# 'inside' ? 2 : 1) 1017 call s:Log(' "' . token . 1018 \'" token found at end of line -> find previous token') 1019 elseif mode ==# 'align_to_begin_element' 1020 " Examples: {{{ 1021 " 1022 " mode == 'align_to_begin_element' && !last_token_of_line 1023 " 1024 " my_func(stored_vcol 1025 " ) % LTI 1026 " 1027 " [Variable, {stored_vcol 1028 " } % LTI 1029 " 1030 " mode == 'align_to_begin_element' && i == 0 1031 " 1032 " ( 1033 " stored_vcol 1034 " ) % LTI 1035 " 1036 " { 1037 " stored_vcol 1038 " } % LTI 1039 " }}} 1040 call s:Log(' "' . token . '" token (whose closing token ' . 1041 \'starts LTI) found -> return') 1042 return curr_vcol 1043 elseif stored_vcol ==# -1 1044 " Examples: {{{ 1045 " 1046 " mode == 'inside' && stored_vcol == -1 && !last_token_of_line 1047 " 1048 " my_func( 1049 " LTI 1050 " [Variable, { 1051 " LTI 1052 " 1053 " mode == 'inside' && stored_vcol == -1 && i == 0 1054 " 1055 " ( 1056 " LTI 1057 " 1058 " { 1059 " LTI 1060 " }}} 1061 call s:Log(' "' . token . 1062 \'" token (which directly precedes LTI) found -> return') 1063 return curr_vcol + 1 1064 else 1065 " Examples: {{{ 1066 " 1067 " mode == 'inside' && stored_vcol != -1 && !last_token_of_line 1068 " 1069 " my_func(stored_vcol, 1070 " LTI 1071 " 1072 " [Variable, {stored_vcol, 1073 " LTI 1074 " 1075 " mode == 'inside' && stored_vcol != -1 && i == 0 1076 " 1077 " (stored_vcol, 1078 " LTI 1079 " 1080 " {stored_vcol, 1081 " LTI 1082 " }}} 1083 call s:Log(' "' . token . 1084 \'" token (whose block contains LTI) found -> return') 1085 return stored_vcol 1086 endif 1087 endif 1088 1089 elseif index(['end', ')', ']', '}', '>>'], token) != -1 1090 1091 " If we can be sure that there is synchronization in the Erlang 1092 " syntax, we use searchpair to make the script quicker. Otherwise we 1093 " just push the token onto the stack and keep parsing. 1094 1095 " No synchronization -> no searchpair optimization 1096 if !exists('b:erlang_syntax_synced') 1097 call s:Push(stack, token) 1098 1099 " We don't have searchpair optimization for '>>' 1100 elseif token ==# '>>' 1101 call s:Push(stack, token) 1102 1103 elseif token ==# 'end' 1104 let [lnum_new, col_new] = s:SearchEndPair(lnum, curr_col) 1105 1106 if lnum_new ==# 0 1107 return s:IndentError('Matching token for "end" not found', 1108 \token, stack) 1109 else 1110 if lnum_new != lnum 1111 call s:Log(' Tokenize for "end" <<<<') 1112 let [lnum, indtokens] = s:TokenizeLine(lnum_new, 'up') 1113 call s:Log(' >>>> Tokenize for "end"') 1114 endif 1115 1116 let [success, i] = s:GetIndtokenAtCol(indtokens, col_new) 1117 if !success | return i | endif 1118 let [token, curr_vcol, curr_col] = indtokens[i] 1119 call s:Log(' Match for "end" in line ' . lnum_new . ': ' . 1120 \string(indtokens[i])) 1121 endif 1122 1123 else " token is one of the following: ')', ']', '}' 1124 1125 call s:Push(stack, token) 1126 1127 " We have to escape '[', because this string will be interpreted as a 1128 " regexp 1129 let open_paren = (token ==# ')' ? '(' : 1130 \token ==# ']' ? '\[' : 1131 \ '{') 1132 1133 let [lnum_new, col_new] = s:SearchPair(lnum, curr_col, 1134 \open_paren, '', token) 1135 1136 if lnum_new ==# 0 1137 return s:IndentError('Matching token not found', 1138 \token, stack) 1139 else 1140 if lnum_new != lnum 1141 call s:Log(' Tokenize the opening paren <<<<') 1142 let [lnum, indtokens] = s:TokenizeLine(lnum_new, 'up') 1143 call s:Log(' >>>>') 1144 endif 1145 1146 let [success, i] = s:GetIndtokenAtCol(indtokens, col_new) 1147 if !success | return i | endif 1148 let [token, curr_vcol, curr_col] = indtokens[i] 1149 call s:Log(' Match in line ' . lnum_new . ': ' . 1150 \string(indtokens[i])) 1151 1152 " Go back to the beginning of the loop and handle the opening paren 1153 continue 1154 endif 1155 endif 1156 1157 elseif token ==# ';' 1158 1159 if empty(stack) 1160 call s:Push(stack, ';') 1161 elseif index([';', '->', 'when', 'end', 'after', 'catch'], 1162 \stack[0]) != -1 1163 " Pass: 1164 " 1165 " - If the stack top is another ';', then one ';' is 1166 " enough. 1167 " - If the stack top is an '->' or a 'when', then we 1168 " should keep that, because they signify the type of the 1169 " LTI (branch, condition or guard). 1170 " - From the indentation point of view, the keyword 1171 " (of/catch/after/end) before the LTI is what counts, so 1172 " if the stack already has a catch/after/end, we don't 1173 " modify it. This way when we reach case/try/receive, 1174 " there will be at most one of/catch/after/end token in 1175 " the stack. 1176 else 1177 return s:UnexpectedToken(token, stack) 1178 endif 1179 1180 elseif token ==# '->' 1181 1182 if empty(stack) && !last_token_of_line 1183 call s:Log(' LTI is in expression after arrow -> return') 1184 return stored_vcol 1185 elseif empty(stack) || stack[0] ==# ';' || stack[0] ==# 'end' 1186 " stack = [';'] -> LTI is either a branch or in a guard 1187 " stack = ['->'] -> LTI is a condition 1188 " stack = ['->', ';'] -> LTI is a branch 1189 call s:Push(stack, '->') 1190 elseif index(['->', 'when', 'end', 'after', 'catch'], stack[0]) != -1 1191 " Pass: 1192 " 1193 " - If the stack top is another '->', then one '->' is 1194 " enough. 1195 " - If the stack top is a 'when', then we should keep 1196 " that, because this signifies that LTI is a in a guard. 1197 " - From the indentation point of view, the keyword 1198 " (of/catch/after/end) before the LTI is what counts, so 1199 " if the stack already has a catch/after/end, we don't 1200 " modify it. This way when we reach case/try/receive, 1201 " there will be at most one of/catch/after/end token in 1202 " the stack. 1203 else 1204 return s:UnexpectedToken(token, stack) 1205 endif 1206 1207 elseif token ==# 'when' 1208 1209 " Pop all ';' from the top of the stack 1210 while !empty(stack) && stack[0] ==# ';' 1211 call s:Pop(stack) 1212 endwhile 1213 1214 if empty(stack) 1215 if semicolon_abscol != '' 1216 let stored_vcol = semicolon_abscol 1217 endif 1218 if !last_token_of_line 1219 " Example: 1220 " when A, 1221 " LTI 1222 let [ret, res] = s:BeginElementFoundIfEmpty(stack, token, curr_vcol, 1223 \stored_vcol, &sw) 1224 if ret | return res | endif 1225 else 1226 " Example: 1227 " when 1228 " LTI 1229 call s:Push(stack, token) 1230 endif 1231 elseif index(['->', 'when', 'end', 'after', 'catch'], stack[0]) != -1 1232 " Pass: 1233 " - If the stack top is another 'when', then one 'when' is 1234 " enough. 1235 " - If the stack top is an '->' or a 'when', then we 1236 " should keep that, because they signify the type of the 1237 " LTI (branch, condition or guard). 1238 " - From the indentation point of view, the keyword 1239 " (of/catch/after/end) before the LTI is what counts, so 1240 " if the stack already has a catch/after/end, we don't 1241 " modify it. This way when we reach case/try/receive, 1242 " there will be at most one of/catch/after/end token in 1243 " the stack. 1244 else 1245 return s:UnexpectedToken(token, stack) 1246 endif 1247 1248 elseif token ==# 'of' || token ==# 'after' || 1249 \ (token ==# 'catch' && !s:IsCatchStandalone(lnum, i)) 1250 1251 if token ==# 'after' 1252 " If LTI is between an 'after' and the corresponding 1253 " 'end', then let's return 1254 let [ret, res] = s:BeginElementFoundIfEmpty(stack, token, curr_vcol, 1255 \stored_vcol, &sw) 1256 if ret | return res | endif 1257 endif 1258 1259 if empty(stack) || stack[0] ==# '->' || stack[0] ==# 'when' 1260 call s:Push(stack, token) 1261 elseif stack[0] ==# 'catch' || stack[0] ==# 'after' || stack[0] ==# 'end' 1262 " Pass: From the indentation point of view, the keyword 1263 " (of/catch/after/end) before the LTI is what counts, so 1264 " if the stack already has a catch/after/end, we don't 1265 " modify it. This way when we reach case/try/receive, 1266 " there will be at most one of/catch/after/end token in 1267 " the stack. 1268 else 1269 return s:UnexpectedToken(token, stack) 1270 endif 1271 1272 elseif token ==# '||' && empty(stack) && !last_token_of_line 1273 1274 call s:Log(' LTI is in expression after "||" -> return') 1275 return stored_vcol 1276 1277 else 1278 call s:Log(' Misc token, stack unchanged = ' . string(stack)) 1279 1280 endif 1281 1282 if empty(stack) || stack[0] ==# '->' || stack[0] ==# 'when' 1283 let stored_vcol = curr_vcol 1284 let semicolon_abscol = '' 1285 call s:Log(' Misc token when the stack is empty or has "->" ' . 1286 \'-> setting stored_vcol to ' . stored_vcol) 1287 elseif stack[0] ==# ';' 1288 let semicolon_abscol = curr_vcol 1289 call s:Log(' Setting semicolon-stored_vcol to ' . stored_vcol) 1290 endif 1291 1292 let i -= 1 1293 call s:Log(' Token processed. stored_vcol=' . stored_vcol) 1294 1295 let last_token_of_line = 0 1296 1297 endwhile " iteration on tokens in a line 1298 1299 call s:Log(' Line analyzed. stored_vcol=' . stored_vcol) 1300 1301 if empty(stack) && stored_vcol != -1 && 1302 \ (!empty(indtokens) && indtokens[0][0] != '<string_end>' && 1303 \ indtokens[0][0] != '<quoted_atom_end>') 1304 call s:Log(' Empty stack at the beginning of the line -> return') 1305 return stored_vcol 1306 endif 1307 1308 let lnum -= 1 1309 1310 endwhile " iteration on lines 1311 1312endfunction 1313 1314" ErlangIndent function {{{1 1315" ===================== 1316 1317function! ErlangIndent() 1318 1319 call s:ClearTokenCacheIfNeeded() 1320 1321 let currline = getline(v:lnum) 1322 call s:Log('Indenting line ' . v:lnum . ': ' . currline) 1323 1324 if s:IsLineStringContinuation(v:lnum) || s:IsLineAtomContinuation(v:lnum) 1325 call s:Log('String or atom continuation found -> ' . 1326 \'leaving indentation unchanged') 1327 return -1 1328 endif 1329 1330 let ml = matchlist(currline, 1331 \'^\(\s*\)\(\%(end\|of\|catch\|after\)\>\|[)\]}]\|>>\)') 1332 1333 " If the line has a special beginning, but not a standalone catch 1334 if !empty(ml) && !(ml[2] ==# 'catch' && s:IsCatchStandalone(v:lnum, 0)) 1335 1336 let curr_col = len(ml[1]) 1337 1338 " If we can be sure that there is synchronization in the Erlang 1339 " syntax, we use searchpair to make the script quicker. 1340 if ml[2] ==# 'end' && exists('b:erlang_syntax_synced') 1341 1342 let [lnum, col] = s:SearchEndPair(v:lnum, curr_col) 1343 1344 if lnum ==# 0 1345 return s:IndentError('Matching token for "end" not found', 1346 \'end', []) 1347 else 1348 call s:Log(' Tokenize for "end" <<<<') 1349 let [lnum, indtokens] = s:TokenizeLine(lnum, 'up') 1350 call s:Log(' >>>> Tokenize for "end"') 1351 1352 let [success, i] = s:GetIndtokenAtCol(indtokens, col) 1353 if !success | return i | endif 1354 let [token, curr_vcol, curr_col] = indtokens[i] 1355 call s:Log(' Match for "end" in line ' . lnum . ': ' . 1356 \string(indtokens[i])) 1357 return curr_vcol 1358 endif 1359 1360 else 1361 1362 call s:Log(" Line type = 'end'") 1363 let new_col = s:ErlangCalcIndent(v:lnum - 1, 1364 \[ml[2], 'align_to_begin_element']) 1365 endif 1366 else 1367 call s:Log(" Line type = 'normal'") 1368 1369 let new_col = s:ErlangCalcIndent(v:lnum - 1, []) 1370 if currline =~# '^\s*when\>' 1371 let new_col += 2 1372 endif 1373 endif 1374 1375 if new_col < -1 1376 call s:Log('WARNING: returning new_col == ' . new_col) 1377 return g:erlang_unexpected_token_indent 1378 endif 1379 1380 return new_col 1381 1382endfunction 1383 1384" Cleanup {{{1 1385" ======= 1386 1387let &cpo = s:cpo_save 1388unlet s:cpo_save 1389 1390" vim: sw=2 et fdm=marker 1391