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