summaryrefslogtreecommitdiffstats
path: root/runtime/indent/erlang.vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/indent/erlang.vim')
-rw-r--r--runtime/indent/erlang.vim1390
1 files changed, 1390 insertions, 0 deletions
diff --git a/runtime/indent/erlang.vim b/runtime/indent/erlang.vim
new file mode 100644
index 0000000..9228f18
--- /dev/null
+++ b/runtime/indent/erlang.vim
@@ -0,0 +1,1390 @@
+" Vim indent file
+" Language: Erlang (http://www.erlang.org)
+" Author: Csaba Hoch <csaba.hoch@gmail.com>
+" Contributors: Edwin Fine <efine145_nospam01 at usa dot net>
+" Pawel 'kTT' Salata <rockplayer.pl@gmail.com>
+" Ricardo Catalinas Jiménez <jimenezrick@gmail.com>
+" Last Update: 2013-Jul-21
+" License: Vim license
+" URL: https://github.com/hcs42/vim-erlang
+
+" Note About Usage:
+" This indentation script works best with the Erlang syntax file created by
+" Kreąimir Marľić (Kresimir Marzic) and maintained by Csaba Hoch.
+
+" Notes About Implementation:
+"
+" - LTI = Line to indent.
+" - The index of the first line is 1, but the index of the first column is 0.
+
+
+" Initialization {{{1
+" ==============
+
+" Only load this indent file when no other was loaded
+" Vim 7 or later is needed
+if exists("b:did_indent") || version < 700
+ finish
+else
+ let b:did_indent = 1
+endif
+
+setlocal indentexpr=ErlangIndent()
+setlocal indentkeys+=0=end,0=of,0=catch,0=after,0=when,0=),0=],0=},0=>>
+
+" Only define the functions once
+if exists("*ErlangIndent")
+ finish
+endif
+
+let s:cpo_save = &cpo
+set cpo&vim
+
+" Logging library {{{1
+" ===============
+
+" Purpose:
+" Logs the given string using the ErlangIndentLog function if it exists.
+" Parameters:
+" s: string
+function! s:Log(s)
+ if exists("*ErlangIndentLog")
+ call ErlangIndentLog(a:s)
+ endif
+endfunction
+
+" Line tokenizer library {{{1
+" ======================
+
+" Indtokens are "indentation tokens".
+
+" Purpose:
+" Calculate the new virtual column after the given segment of a line.
+" Parameters:
+" line: string
+" first_index: integer -- the index of the first character of the segment
+" last_index: integer -- the index of the last character of the segment
+" vcol: integer -- the virtual column of the first character of the token
+" tabstop: integer -- the value of the 'tabstop' option to be used
+" Returns:
+" vcol: integer
+" Example:
+" " index: 0 12 34567
+" " vcol: 0 45 89
+" s:CalcVCol("\t'\tx', b", 1, 4, 4) -> 10
+function! s:CalcVCol(line, first_index, last_index, vcol, tabstop)
+
+ " We copy the relevent segment of the line, otherwise if the line were
+ " e.g. `"\t", term` then the else branch below would consume the `", term`
+ " part at once.
+ let line = a:line[a:first_index : a:last_index]
+
+ let i = 0
+ let last_index = a:last_index - a:first_index
+ let vcol = a:vcol
+
+ while 0 <= i && i <= last_index
+
+ if line[i] ==# "\t"
+ " Example (when tabstop == 4):
+ "
+ " vcol + tab -> next_vcol
+ " 0 + tab -> 4
+ " 1 + tab -> 4
+ " 2 + tab -> 4
+ " 3 + tab -> 4
+ " 4 + tab -> 8
+ "
+ " next_i - i == the number of tabs
+ let next_i = matchend(line, '\t*', i + 1)
+ let vcol = (vcol / a:tabstop + (next_i - i)) * a:tabstop
+ call s:Log('new vcol after tab: '. vcol)
+ else
+ let next_i = matchend(line, '[^\t]*', i + 1)
+ let vcol += next_i - i
+ call s:Log('new vcol after other: '. vcol)
+ endif
+ let i = next_i
+ endwhile
+
+ return vcol
+endfunction
+
+" Purpose:
+" Go through the whole line and return the tokens in the line.
+" Parameters:
+" line: string -- the line to be examined
+" string_continuation: bool
+" atom_continuation: bool
+" Returns:
+" indtokens = [indtoken]
+" indtoken = [token, vcol, col]
+" token = string (examples: 'begin', '<variable>', '}')
+" vcol = integer (the virtual column of the first character of the token)
+" col = integer
+function! s:GetTokensFromLine(line, string_continuation, atom_continuation,
+ \tabstop)
+
+ let linelen = strlen(a:line) " The length of the line
+ let i = 0 " The index of the current character in the line
+ let vcol = 0 " The virtual column of the current character
+ let indtokens = []
+
+ if a:string_continuation
+ let i = matchend(a:line, '^\%([^"\\]\|\\.\)*"', 0)
+ if i ==# -1
+ call s:Log(' Whole line is string continuation -> ignore')
+ return []
+ else
+ let vcol = s:CalcVCol(a:line, 0, i - 1, 0, a:tabstop)
+ call add(indtokens, ['<string_end>', vcol, i])
+ endif
+ elseif a:atom_continuation
+ let i = matchend(a:line, "^\\%([^'\\\\]\\|\\\\.\\)*'", 0)
+ if i ==# -1
+ call s:Log(' Whole line is quoted atom continuation -> ignore')
+ return []
+ else
+ let vcol = s:CalcVCol(a:line, 0, i - 1, 0, a:tabstop)
+ call add(indtokens, ['<quoted_atom_end>', vcol, i])
+ endif
+ endif
+
+ while 0 <= i && i < linelen
+
+ let next_vcol = ''
+
+ " Spaces
+ if a:line[i] ==# ' '
+ let next_i = matchend(a:line, ' *', i + 1)
+
+ " Tabs
+ elseif a:line[i] ==# "\t"
+ let next_i = matchend(a:line, '\t*', i + 1)
+
+ " See example in s:CalcVCol
+ let next_vcol = (vcol / a:tabstop + (next_i - i)) * a:tabstop
+
+ " Comment
+ elseif a:line[i] ==# '%'
+ let next_i = linelen
+
+ " String token: "..."
+ elseif a:line[i] ==# '"'
+ let next_i = matchend(a:line, '\%([^"\\]\|\\.\)*"', i + 1)
+ if next_i ==# -1
+ call add(indtokens, ['<string_start>', vcol, i])
+ else
+ let next_vcol = s:CalcVCol(a:line, i, next_i - 1, vcol, a:tabstop)
+ call add(indtokens, ['<string>', vcol, i])
+ endif
+
+ " Quoted atom token: '...'
+ elseif a:line[i] ==# "'"
+ let next_i = matchend(a:line, "\\%([^'\\\\]\\|\\\\.\\)*'", i + 1)
+ if next_i ==# -1
+ call add(indtokens, ['<quoted_atom_start>', vcol, i])
+ else
+ let next_vcol = s:CalcVCol(a:line, i, next_i - 1, vcol, a:tabstop)
+ call add(indtokens, ['<quoted_atom>', vcol, i])
+ endif
+
+ " Keyword or atom or variable token or number
+ elseif a:line[i] =~# '[a-zA-Z_@0-9]'
+ let next_i = matchend(a:line,
+ \'[[:alnum:]_@:]*\%(\s*#\s*[[:alnum:]_@:]*\)\=',
+ \i + 1)
+ call add(indtokens, [a:line[(i):(next_i - 1)], vcol, i])
+
+ " Character token: $<char> (as in: $a)
+ elseif a:line[i] ==# '$'
+ call add(indtokens, ['$.', vcol, i])
+ let next_i = i + 2
+
+ " Dot token: .
+ elseif a:line[i] ==# '.'
+
+ let next_i = i + 1
+
+ if i + 1 ==# linelen || a:line[i + 1] =~# '[[:blank:]%]'
+ " End of clause token: . (as in: f() -> ok.)
+ call add(indtokens, ['<end_of_clause>', vcol, i])
+
+ else
+ " Possibilities:
+ " - Dot token in float: . (as in: 3.14)
+ " - Dot token in record: . (as in: #myrec.myfield)
+ call add(indtokens, ['.', vcol, i])
+ endif
+
+ " Equal sign
+ elseif a:line[i] ==# '='
+ " This is handled separately so that "=<<" will be parsed as
+ " ['=', '<<'] instead of ['=<', '<']. Although Erlang parses it
+ " currently in the latter way, that may be fixed some day.
+ call add(indtokens, [a:line[i], vcol, i])
+ let next_i = i + 1
+
+ " Three-character tokens
+ elseif i + 1 < linelen &&
+ \ index(['=:=', '=/='], a:line[i : i + 1]) != -1
+ call add(indtokens, [a:line[i : i + 1], vcol, i])
+ let next_i = i + 2
+
+ " Two-character tokens
+ elseif i + 1 < linelen &&
+ \ index(['->', '<<', '>>', '||', '==', '/=', '=<', '>=', '++', '--',
+ \ '::'],
+ \ a:line[i : i + 1]) != -1
+ call add(indtokens, [a:line[i : i + 1], vcol, i])
+ let next_i = i + 2
+
+ " Other character: , ; < > ( ) [ ] { } # + - * / : ? = ! |
+ else
+ call add(indtokens, [a:line[i], vcol, i])
+ let next_i = i + 1
+
+ endif
+
+ if next_vcol ==# ''
+ let vcol += next_i - i
+ else
+ let vcol = next_vcol
+ endif
+
+ let i = next_i
+
+ endwhile
+
+ return indtokens
+
+endfunction
+
+" TODO: doc, handle "not found" case
+function! s:GetIndtokenAtCol(indtokens, col)
+ let i = 0
+ while i < len(a:indtokens)
+ if a:indtokens[i][2] ==# a:col
+ return [1, i]
+ elseif a:indtokens[i][2] > a:col
+ return [0, s:IndentError('No token at col ' . a:col . ', ' .
+ \'indtokens = ' . string(a:indtokens),
+ \'', '')]
+ endif
+ let i += 1
+ endwhile
+ return [0, s:IndentError('No token at col ' . a:col . ', ' .
+ \'indtokens = ' . string(a:indtokens),
+ \'', '')]
+endfunction
+
+" Stack library {{{1
+" =============
+
+" Purpose:
+" Push a token onto the parser's stack.
+" Parameters:
+" stack: [token]
+" token: string
+function! s:Push(stack, token)
+ call s:Log(' Stack Push: "' . a:token . '" into ' . string(a:stack))
+ call insert(a:stack, a:token)
+endfunction
+
+" Purpose:
+" Pop a token from the parser's stack.
+" Parameters:
+" stack: [token]
+" token: string
+" Returns:
+" token: string -- the removed element
+function! s:Pop(stack)
+ let head = remove(a:stack, 0)
+ call s:Log(' Stack Pop: "' . head . '" from ' . string(a:stack))
+ return head
+endfunction
+
+" Library for accessing and storing tokenized lines {{{1
+" =================================================
+
+" The Erlang token cache: an `lnum -> indtokens` dictionary that stores the
+" tokenized lines.
+let s:all_tokens = {}
+let s:file_name = ''
+let s:last_changedtick = -1
+
+" Purpose:
+" Clear the Erlang token cache if we have a different file or the file has
+" been changed since the last indentation.
+function! s:ClearTokenCacheIfNeeded()
+ let file_name = expand('%:p')
+ if file_name != s:file_name ||
+ \ b:changedtick != s:last_changedtick
+ let s:file_name = file_name
+ let s:last_changedtick = b:changedtick
+ let s:all_tokens = {}
+ endif
+endfunction
+
+" Purpose:
+" Return the tokens of line `lnum`, if that line is not empty. If it is
+" empty, find the first non-empty line in the given `direction` and return
+" the tokens of that line.
+" Parameters:
+" lnum: integer
+" direction: 'up' | 'down'
+" Returns:
+" result: [] -- the result is an empty list if we hit the beginning or end
+" of the file
+" | [lnum, indtokens]
+" lnum: integer -- the index of the non-empty line that was found and
+" tokenized
+" indtokens: [indtoken] -- the tokens of line `lnum`
+function! s:TokenizeLine(lnum, direction)
+
+ call s:Log('Tokenizing starts from line ' . a:lnum)
+ if a:direction ==# 'up'
+ let lnum = prevnonblank(a:lnum)
+ else " a:direction ==# 'down'
+ let lnum = nextnonblank(a:lnum)
+ endif
+
+ " We hit the beginning or end of the file
+ if lnum ==# 0
+ let indtokens = []
+ call s:Log(' We hit the beginning or end of the file.')
+
+ " The line has already been parsed
+ elseif has_key(s:all_tokens, lnum)
+ let indtokens = s:all_tokens[lnum]
+ call s:Log('Cached line ' . lnum . ': ' . getline(lnum))
+ call s:Log(" Tokens in the line:\n - " . join(indtokens, "\n - "))
+
+ " The line should be parsed now
+ else
+
+ " Parse the line
+ let line = getline(lnum)
+ let string_continuation = s:IsLineStringContinuation(lnum)
+ let atom_continuation = s:IsLineAtomContinuation(lnum)
+ let indtokens = s:GetTokensFromLine(line, string_continuation,
+ \atom_continuation, &tabstop)
+ let s:all_tokens[lnum] = indtokens
+ call s:Log('Tokenizing line ' . lnum . ': ' . line)
+ call s:Log(" Tokens in the line:\n - " . join(indtokens, "\n - "))
+
+ endif
+
+ return [lnum, indtokens]
+endfunction
+
+" Purpose:
+" As a helper function for PrevIndToken and NextIndToken, the FindIndToken
+" function finds the first line with at least one token in the given
+" direction.
+" Parameters:
+" lnum: integer
+" direction: 'up' | 'down'
+" Returns:
+" result: [] -- the result is an empty list if we hit the beginning or end
+" of the file
+" | indtoken
+function! s:FindIndToken(lnum, dir)
+ let lnum = a:lnum
+ while 1
+ let lnum += (a:dir ==# 'up' ? -1 : 1)
+ let [lnum, indtokens] = s:TokenizeLine(lnum, a:dir)
+ if lnum ==# 0
+ " We hit the beginning or end of the file
+ return []
+ elseif !empty(indtokens)
+ return indtokens[a:dir ==# 'up' ? -1 : 0]
+ endif
+ endwhile
+endfunction
+
+" Purpose:
+" Find the token that directly precedes the given token.
+" Parameters:
+" lnum: integer -- the line of the given token
+" i: the index of the given token within line `lnum`
+" Returns:
+" result = [] -- the result is an empty list if the given token is the first
+" token of the file
+" | indtoken
+function! s:PrevIndToken(lnum, i)
+ call s:Log(' PrevIndToken called: lnum=' . a:lnum . ', i =' . a:i)
+
+ " If the current line has a previous token, return that
+ if a:i > 0
+ return s:all_tokens[a:lnum][a:i - 1]
+ else
+ return s:FindIndToken(a:lnum, 'up')
+ endif
+endfunction
+
+" Purpose:
+" Find the token that directly succeeds the given token.
+" Parameters:
+" lnum: integer -- the line of the given token
+" i: the index of the given token within line `lnum`
+" Returns:
+" result = [] -- the result is an empty list if the given token is the last
+" token of the file
+" | indtoken
+function! s:NextIndToken(lnum, i)
+ call s:Log(' NextIndToken called: lnum=' . a:lnum . ', i =' . a:i)
+
+ " If the current line has a next token, return that
+ if len(s:all_tokens[a:lnum]) > a:i + 1
+ return s:all_tokens[a:lnum][a:i + 1]
+ else
+ return s:FindIndToken(a:lnum, 'down')
+ endif
+endfunction
+
+" ErlangCalcIndent helper functions {{{1
+" =================================
+
+" Purpose:
+" This function is called when the parser encounters a syntax error.
+"
+" If we encounter a syntax error, we return
+" g:erlang_unexpected_token_indent, which is -1 by default. This means that
+" the indentation of the LTI will not be changed.
+" Parameter:
+" msg: string
+" token: string
+" stack: [token]
+" Returns:
+" indent: integer
+function! s:IndentError(msg, token, stack)
+ call s:Log('Indent error: ' . a:msg . ' -> return')
+ call s:Log(' Token = ' . a:token . ', ' .
+ \' stack = ' . string(a:stack))
+ return g:erlang_unexpected_token_indent
+endfunction
+
+" Purpose:
+" This function is called when the parser encounters an unexpected token,
+" and the parser will return the number given back by UnexpectedToken.
+"
+" If we encounter an unexpected token, we return
+" g:erlang_unexpected_token_indent, which is -1 by default. This means that
+" the indentation of the LTI will not be changed.
+" Parameter:
+" token: string
+" stack: [token]
+" Returns:
+" indent: integer
+function! s:UnexpectedToken(token, stack)
+ call s:Log(' Unexpected token ' . a:token . ', stack = ' .
+ \string(a:stack) . ' -> return')
+ return g:erlang_unexpected_token_indent
+endfunction
+
+if !exists('g:erlang_unexpected_token_indent')
+ let g:erlang_unexpected_token_indent = -1
+endif
+
+" Purpose:
+" Return whether the given line starts with a string continuation.
+" Parameter:
+" lnum: integer
+" Returns:
+" result: bool
+" Example:
+" f() -> % IsLineStringContinuation = false
+" "This is a % IsLineStringContinuation = false
+" multiline % IsLineStringContinuation = true
+" string". % IsLineStringContinuation = true
+function! s:IsLineStringContinuation(lnum)
+ if has('syntax_items')
+ return synIDattr(synID(a:lnum, 1, 0), 'name') =~# '^erlangString'
+ else
+ return 0
+ endif
+endfunction
+
+" Purpose:
+" Return whether the given line starts with an atom continuation.
+" Parameter:
+" lnum: integer
+" Returns:
+" result: bool
+" Example:
+" 'function with % IsLineAtomContinuation = true, but should be false
+" weird name'() -> % IsLineAtomContinuation = true
+" ok. % IsLineAtomContinuation = false
+function! s:IsLineAtomContinuation(lnum)
+ if has('syntax_items')
+ return synIDattr(synID(a:lnum, 1, 0), 'name') =~# '^erlangQuotedAtom'
+ else
+ return 0
+ endif
+endfunction
+
+" Purpose:
+" Return whether the 'catch' token (which should be the `i`th token in line
+" `lnum`) is standalone or part of a try-catch block, based on the preceding
+" token.
+" Parameters:
+" lnum: integer
+" i: integer
+" Return:
+" is_standalone: bool
+function! s:IsCatchStandalone(lnum, i)
+ call s:Log(' IsCatchStandalone called: lnum=' . a:lnum . ', i=' . a:i)
+ let prev_indtoken = s:PrevIndToken(a:lnum, a:i)
+
+ " If we hit the beginning of the file, it is not a catch in a try block
+ if prev_indtoken == []
+ return 1
+ endif
+
+ let prev_token = prev_indtoken[0]
+
+ if prev_token =~# '[A-Z_@0-9]'
+ let is_standalone = 0
+ elseif prev_token =~# '[a-z]'
+ if index(['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl',
+ \ 'bsr', 'bxor', 'case', 'catch', 'div', 'not', 'or', 'orelse',
+ \ 'rem', 'try', 'xor'], prev_token) != -1
+ " If catch is after these keywords, it is standalone
+ let is_standalone = 1
+ else
+ " If catch is after another keyword (e.g. 'end') or an atom, it is
+ " part of try-catch.
+ "
+ " Keywords:
+ " - may precede 'catch': end
+ " - may not precede 'catch': fun if of receive when
+ " - unused: cond let query
+ let is_standalone = 0
+ endif
+ elseif index([')', ']', '}', '<string>', '<string_end>', '<quoted_atom>',
+ \ '<quoted_atom_end>', '$.'], prev_token) != -1
+ let is_standalone = 0
+ else
+ " This 'else' branch includes the following tokens:
+ " -> == /= =< < >= > =:= =/= + - * / ++ -- :: < > ; ( [ { ? = ! . |
+ let is_standalone = 1
+ endif
+
+ call s:Log(' "catch" preceded by "' . prev_token . '" -> catch ' .
+ \(is_standalone ? 'is standalone' : 'belongs to try-catch'))
+ return is_standalone
+
+endfunction
+
+" Purpose:
+" This function is called when a begin-type element ('begin', 'case',
+" '[', '<<', etc.) is found. It asks the caller to return if the stack
+" Parameters:
+" stack: [token]
+" token: string
+" curr_vcol: integer
+" stored_vcol: integer
+" sw: integer -- number of spaces to be used after the begin element as
+" indentation
+" Returns:
+" result: [should_return, indent]
+" should_return: bool -- if true, the caller should return `indent` to Vim
+" indent -- integer
+function! s:BeginElementFoundIfEmpty(stack, token, curr_vcol, stored_vcol, sw)
+ if empty(a:stack)
+ if a:stored_vcol ==# -1
+ call s:Log(' "' . a:token . '" directly preceeds LTI -> return')
+ return [1, a:curr_vcol + a:sw]
+ else
+ call s:Log(' "' . a:token .
+ \'" token (whose expression includes LTI) found -> return')
+ return [1, a:stored_vcol]
+ endif
+ else
+ return [0, 0]
+ endif
+endfunction
+
+" Purpose:
+" This function is called when a begin-type element ('begin', 'case', '[',
+" '<<', etc.) is found, and in some cases when 'after' and 'when' is found.
+" It asks the caller to return if the stack is already empty.
+" Parameters:
+" stack: [token]
+" token: string
+" curr_vcol: integer
+" stored_vcol: integer
+" end_token: end token that belongs to the begin element found (e.g. if the
+" begin element is 'begin', the end token is 'end')
+" sw: integer -- number of spaces to be used after the begin element as
+" indentation
+" Returns:
+" result: [should_return, indent]
+" should_return: bool -- if true, the caller should return `indent` to Vim
+" indent -- integer
+function! s:BeginElementFound(stack, token, curr_vcol, stored_vcol, end_token, sw)
+
+ " Return 'return' if the stack is empty
+ let [ret, res] = s:BeginElementFoundIfEmpty(a:stack, a:token, a:curr_vcol,
+ \a:stored_vcol, a:sw)
+ if ret | return [ret, res] | endif
+
+ if a:stack[0] ==# a:end_token
+ call s:Log(' "' . a:token . '" pops "' . a:end_token . '"')
+ call s:Pop(a:stack)
+ if !empty(a:stack) && a:stack[0] ==# 'align_to_begin_element'
+ call s:Pop(a:stack)
+ if empty(a:stack)
+ return [1, a:curr_vcol]
+ else
+ return [1, s:UnexpectedToken(a:token, a:stack)]
+ endif
+ else
+ return [0, 0]
+ endif
+ else
+ return [1, s:UnexpectedToken(a:token, a:stack)]
+ endif
+endfunction
+
+" Purpose:
+" This function is called when we hit the beginning of a file or an
+" end-of-clause token -- i.e. when we found the beginning of the current
+" clause.
+"
+" If the stack contains an '->' or 'when', this means that we can return
+" now, since we were looking for the beginning of the clause.
+" Parameters:
+" stack: [token]
+" token: string
+" stored_vcol: integer
+" Returns:
+" result: [should_return, indent]
+" should_return: bool -- if true, the caller should return `indent` to Vim
+" indent -- integer
+function! s:BeginningOfClauseFound(stack, token, stored_vcol)
+ if !empty(a:stack) && a:stack[0] ==# 'when'
+ call s:Log(' BeginningOfClauseFound: "when" found in stack')
+ call s:Pop(a:stack)
+ if empty(a:stack)
+ call s:Log(' Stack is ["when"], so LTI is in a guard -> return')
+ return [1, a:stored_vcol + shiftwidth() + 2]
+ else
+ return [1, s:UnexpectedToken(a:token, a:stack)]
+ endif
+ elseif !empty(a:stack) && a:stack[0] ==# '->'
+ call s:Log(' BeginningOfClauseFound: "->" found in stack')
+ call s:Pop(a:stack)
+ if empty(a:stack)
+ call s:Log(' Stack is ["->"], so LTI is in function body -> return')
+ return [1, a:stored_vcol + shiftwidth()]
+ elseif a:stack[0] ==# ';'
+ call s:Pop(a:stack)
+ if empty(a:stack)
+ call s:Log(' Stack is ["->", ";"], so LTI is in a function head ' .
+ \'-> return')
+ return [0, a:stored_vcol]
+ else
+ return [1, s:UnexpectedToken(a:token, a:stack)]
+ endif
+ else
+ return [1, s:UnexpectedToken(a:token, a:stack)]
+ endif
+ else
+ return [0, 0]
+ endif
+endfunction
+
+let g:erlang_indent_searchpair_timeout = 2000
+
+" TODO
+function! s:SearchPair(lnum, curr_col, start, middle, end)
+ call cursor(a:lnum, a:curr_col + 1)
+ let [lnum_new, col1_new] =
+ \searchpairpos(a:start, a:middle, a:end, 'bW',
+ \'synIDattr(synID(line("."), col("."), 0), "name") ' .
+ \'=~? "string\\|quotedatom\\|todo\\|comment\\|' .
+ \'erlangmodifier"',
+ \0, g:erlang_indent_searchpair_timeout)
+ return [lnum_new, col1_new - 1]
+endfunction
+
+function! s:SearchEndPair(lnum, curr_col)
+ return s:SearchPair(
+ \ a:lnum, a:curr_col,
+ \ '\C\<\%(case\|try\|begin\|receive\|if\)\>\|' .
+ \ '\<fun\>\%(\s\|\n\|%.*$\)*(',
+ \ '',
+ \ '\<end\>')
+endfunction
+
+" ErlangCalcIndent {{{1
+" ================
+
+" Purpose:
+" Calculate the indentation of the given line.
+" Parameters:
+" lnum: integer -- index of the line for which the indentation should be
+" calculated
+" stack: [token] -- initial stack
+" Return:
+" indent: integer -- if -1, that means "don't change the indentation";
+" otherwise it means "indent the line with `indent`
+" number of spaces or equivalent tabs"
+function! s:ErlangCalcIndent(lnum, stack)
+ let res = s:ErlangCalcIndent2(a:lnum, a:stack)
+ call s:Log("ErlangCalcIndent returned: " . res)
+ return res
+endfunction
+
+function! s:ErlangCalcIndent2(lnum, stack)
+
+ let lnum = a:lnum
+ let stored_vcol = -1 " Virtual column of the first character of the token that
+ " we currently think we might align to.
+ let mode = 'normal'
+ let stack = a:stack
+ let semicolon_abscol = ''
+
+ " Walk through the lines of the buffer backwards (starting from the
+ " previous line) until we can decide how to indent the current line.
+ while 1
+
+ let [lnum, indtokens] = s:TokenizeLine(lnum, 'up')
+
+ " Hit the start of the file
+ if lnum ==# 0
+ let [ret, res] = s:BeginningOfClauseFound(stack, 'beginning_of_file',
+ \stored_vcol)
+ if ret | return res | endif
+
+ return 0
+ endif
+
+ let i = len(indtokens) - 1
+ let last_token_of_line = 1
+
+ while i >= 0
+
+ let [token, curr_vcol, curr_col] = indtokens[i]
+ call s:Log(' Analyzing the following token: ' . string(indtokens[i]))
+
+ if len(stack) > 256 " TODO: magic number
+ return s:IndentError('Stack too long', token, stack)
+ endif
+
+ if token ==# '<end_of_clause>'
+ let [ret, res] = s:BeginningOfClauseFound(stack, token, stored_vcol)
+ if ret | return res | endif
+
+ if stored_vcol ==# -1
+ call s:Log(' End of clause directly preceeds LTI -> return')
+ return 0
+ else
+ call s:Log(' End of clause (but not end of line) -> return')
+ return stored_vcol
+ endif
+
+ elseif stack == ['prev_term_plus']
+ if token =~# '[a-zA-Z_@]' ||
+ \ token ==# '<string>' || token ==# '<string_start>' ||
+ \ token ==# '<quoted_atom>' || token ==# '<quoted_atom_start>'
+ call s:Log(' previous token found: curr_vcol + plus = ' .
+ \curr_vcol . " + " . plus)
+ return curr_vcol + plus
+ endif
+
+ elseif token ==# 'begin'
+ let [ret, res] = s:BeginElementFound(stack, token, curr_vcol,
+ \stored_vcol, 'end', shiftwidth())
+ if ret | return res | endif
+
+ " case EXPR of BRANCHES end
+ " try EXPR catch BRANCHES end
+ " try EXPR after BODY end
+ " try EXPR catch BRANCHES after BODY end
+ " try EXPR of BRANCHES catch BRANCHES end
+ " try EXPR of BRANCHES after BODY end
+ " try EXPR of BRANCHES catch BRANCHES after BODY end
+ " receive BRANCHES end
+ " receive BRANCHES after BRANCHES end
+
+ " This branch is not Emacs-compatible
+ elseif (index(['of', 'receive', 'after', 'if'], token) != -1 ||
+ \ (token ==# 'catch' && !s:IsCatchStandalone(lnum, i))) &&
+ \ !last_token_of_line &&
+ \ (empty(stack) || stack ==# ['when'] || stack ==# ['->'] ||
+ \ stack ==# ['->', ';'])
+
+ " If we are after of/receive, but these are not the last
+ " tokens of the line, we want to indent like this:
+ "
+ " % stack == []
+ " receive stored_vcol,
+ " LTI
+ "
+ " % stack == ['->', ';']
+ " receive stored_vcol ->
+ " B;
+ " LTI
+ "
+ " % stack == ['->']
+ " receive stored_vcol ->
+ " LTI
+ "
+ " % stack == ['when']
+ " receive stored_vcol when
+ " LTI
+
+ " stack = [] => LTI is a condition
+ " stack = ['->'] => LTI is a branch
+ " stack = ['->', ';'] => LTI is a condition
+ " stack = ['when'] => LTI is a guard
+ if empty(stack) || stack == ['->', ';']
+ call s:Log(' LTI is in a condition after ' .
+ \'"of/receive/after/if/catch" -> return')
+ return stored_vcol
+ elseif stack == ['->']
+ call s:Log(' LTI is in a branch after ' .
+ \'"of/receive/after/if/catch" -> return')
+ return stored_vcol + shiftwidth()
+ elseif stack == ['when']
+ call s:Log(' LTI is in a guard after ' .
+ \'"of/receive/after/if/catch" -> return')
+ return stored_vcol + shiftwidth()
+ else
+ return s:UnexpectedToken(token, stack)
+ endif
+
+ elseif index(['case', 'if', 'try', 'receive'], token) != -1
+
+ " stack = [] => LTI is a condition
+ " stack = ['->'] => LTI is a branch
+ " stack = ['->', ';'] => LTI is a condition
+ " stack = ['when'] => LTI is in a guard
+ if empty(stack)
+ " pass
+ elseif (token ==# 'case' && stack[0] ==# 'of') ||
+ \ (token ==# 'if') ||
+ \ (token ==# 'try' && (stack[0] ==# 'of' ||
+ \ stack[0] ==# 'catch' ||
+ \ stack[0] ==# 'after')) ||
+ \ (token ==# 'receive')
+
+ " From the indentation point of view, the keyword
+ " (of/catch/after/end) before the LTI is what counts, so
+ " when we reached these tokens, and the stack already had
+ " a catch/after/end, we didn't modify it.
+ "
+ " This way when we reach case/try/receive (i.e. now),
+ " there is at most one of/catch/after/end token in the
+ " stack.
+ if token ==# 'case' || token ==# 'try' ||
+ \ (token ==# 'receive' && stack[0] ==# 'after')
+ call s:Pop(stack)
+ endif
+
+ if empty(stack)
+ call s:Log(' LTI is in a condition; matching ' .
+ \'"case/if/try/receive" found')
+ let stored_vcol = curr_vcol + shiftwidth()
+ elseif stack[0] ==# 'align_to_begin_element'
+ call s:Pop(stack)
+ let stored_vcol = curr_vcol
+ elseif len(stack) > 1 && stack[0] ==# '->' && stack[1] ==# ';'
+ call s:Log(' LTI is in a condition; matching ' .
+ \'"case/if/try/receive" found')
+ call s:Pop(stack)
+ call s:Pop(stack)
+ let stored_vcol = curr_vcol + shiftwidth()
+ elseif stack[0] ==# '->'
+ call s:Log(' LTI is in a branch; matching ' .
+ \'"case/if/try/receive" found')
+ call s:Pop(stack)
+ let stored_vcol = curr_vcol + 2 * shiftwidth()
+ elseif stack[0] ==# 'when'
+ call s:Log(' LTI is in a guard; matching ' .
+ \'"case/if/try/receive" found')
+ call s:Pop(stack)
+ let stored_vcol = curr_vcol + 2 * shiftwidth() + 2
+ endif
+
+ endif
+
+ let [ret, res] = s:BeginElementFound(stack, token, curr_vcol,
+ \stored_vcol, 'end', shiftwidth())
+ if ret | return res | endif
+
+ elseif token ==# 'fun'
+ let next_indtoken = s:NextIndToken(lnum, i)
+ call s:Log(' Next indtoken = ' . string(next_indtoken))
+
+ if !empty(next_indtoken) && next_indtoken[0] ==# '('
+ " We have an anonymous function definition
+ " (e.g. "fun () -> ok end")
+
+ " stack = [] => LTI is a condition
+ " stack = ['->'] => LTI is a branch
+ " stack = ['->', ';'] => LTI is a condition
+ " stack = ['when'] => LTI is in a guard
+ if empty(stack)
+ call s:Log(' LTI is in a condition; matching "fun" found')
+ let stored_vcol = curr_vcol + shiftwidth()
+ elseif len(stack) > 1 && stack[0] ==# '->' && stack[1] ==# ';'
+ call s:Log(' LTI is in a condition; matching "fun" found')
+ call s:Pop(stack)
+ call s:Pop(stack)
+ elseif stack[0] ==# '->'
+ call s:Log(' LTI is in a branch; matching "fun" found')
+ call s:Pop(stack)
+ let stored_vcol = curr_vcol + 2 * shiftwidth()
+ elseif stack[0] ==# 'when'
+ call s:Log(' LTI is in a guard; matching "fun" found')
+ call s:Pop(stack)
+ let stored_vcol = curr_vcol + 2 * shiftwidth() + 2
+ endif
+
+ let [ret, res] = s:BeginElementFound(stack, token, curr_vcol,
+ \stored_vcol, 'end', shiftwidth())
+ if ret | return res | endif
+ else
+ " Pass: we have a function reference (e.g. "fun f/0")
+ endif
+
+ elseif token ==# '['
+ " Emacs compatibility
+ let [ret, res] = s:BeginElementFound(stack, token, curr_vcol,
+ \stored_vcol, ']', 1)
+ if ret | return res | endif
+
+ elseif token ==# '<<'
+ " Emacs compatibility
+ let [ret, res] = s:BeginElementFound(stack, token, curr_vcol,
+ \stored_vcol, '>>', 2)
+ if ret | return res | endif
+
+ elseif token ==# '(' || token ==# '{'
+
+ let end_token = (token ==# '(' ? ')' :
+ \token ==# '{' ? '}' : 'error')
+
+ if empty(stack)
+ " We found the opening paren whose block contains the LTI.
+ let mode = 'inside'
+ elseif stack[0] ==# end_token
+ call s:Log(' "' . token . '" pops "' . end_token . '"')
+ call s:Pop(stack)
+
+ if !empty(stack) && stack[0] ==# 'align_to_begin_element'
+ " We found the opening paren whose closing paren
+ " starts LTI
+ let mode = 'align_to_begin_element'
+ else
+ " We found the opening pair for a closing paren that
+ " was already in the stack.
+ let mode = 'outside'
+ endif
+ else
+ return s:UnexpectedToken(token, stack)
+ endif
+
+ if mode ==# 'inside' || mode ==# 'align_to_begin_element'
+
+ if last_token_of_line && i != 0
+ " Examples: {{{
+ "
+ " mode == 'inside':
+ "
+ " my_func(
+ " LTI
+ "
+ " [Variable, {
+ " LTI
+ "
+ " mode == 'align_to_begin_element':
+ "
+ " my_func(
+ " Params
+ " ) % LTI
+ "
+ " [Variable, {
+ " Terms
+ " } % LTI
+ " }}}
+ let stack = ['prev_term_plus']
+ let plus = (mode ==# 'inside' ? 2 : 1)
+ call s:Log(' "' . token .
+ \'" token found at end of line -> find previous token')
+ elseif mode ==# 'align_to_begin_element'
+ " Examples: {{{
+ "
+ " mode == 'align_to_begin_element' && !last_token_of_line
+ "
+ " my_func(stored_vcol
+ " ) % LTI
+ "
+ " [Variable, {stored_vcol
+ " } % LTI
+ "
+ " mode == 'align_to_begin_element' && i == 0
+ "
+ " (
+ " stored_vcol
+ " ) % LTI
+ "
+ " {
+ " stored_vcol
+ " } % LTI
+ " }}}
+ call s:Log(' "' . token . '" token (whose closing token ' .
+ \'starts LTI) found -> return')
+ return curr_vcol
+ elseif stored_vcol ==# -1
+ " Examples: {{{
+ "
+ " mode == 'inside' && stored_vcol == -1 && !last_token_of_line
+ "
+ " my_func(
+ " LTI
+ " [Variable, {
+ " LTI
+ "
+ " mode == 'inside' && stored_vcol == -1 && i == 0
+ "
+ " (
+ " LTI
+ "
+ " {
+ " LTI
+ " }}}
+ call s:Log(' "' . token .
+ \'" token (which directly precedes LTI) found -> return')
+ return curr_vcol + 1
+ else
+ " Examples: {{{
+ "
+ " mode == 'inside' && stored_vcol != -1 && !last_token_of_line
+ "
+ " my_func(stored_vcol,
+ " LTI
+ "
+ " [Variable, {stored_vcol,
+ " LTI
+ "
+ " mode == 'inside' && stored_vcol != -1 && i == 0
+ "
+ " (stored_vcol,
+ " LTI
+ "
+ " {stored_vcol,
+ " LTI
+ " }}}
+ call s:Log(' "' . token .
+ \'" token (whose block contains LTI) found -> return')
+ return stored_vcol
+ endif
+ endif
+
+ elseif index(['end', ')', ']', '}', '>>'], token) != -1
+
+ " If we can be sure that there is synchronization in the Erlang
+ " syntax, we use searchpair to make the script quicker. Otherwise we
+ " just push the token onto the stack and keep parsing.
+
+ " No synchronization -> no searchpair optimization
+ if !exists('b:erlang_syntax_synced')
+ call s:Push(stack, token)
+
+ " We don't have searchpair optimization for '>>'
+ elseif token ==# '>>'
+ call s:Push(stack, token)
+
+ elseif token ==# 'end'
+ let [lnum_new, col_new] = s:SearchEndPair(lnum, curr_col)
+
+ if lnum_new ==# 0
+ return s:IndentError('Matching token for "end" not found',
+ \token, stack)
+ else
+ if lnum_new != lnum
+ call s:Log(' Tokenize for "end" <<<<')
+ let [lnum, indtokens] = s:TokenizeLine(lnum_new, 'up')
+ call s:Log(' >>>> Tokenize for "end"')
+ endif
+
+ let [success, i] = s:GetIndtokenAtCol(indtokens, col_new)
+ if !success | return i | endif
+ let [token, curr_vcol, curr_col] = indtokens[i]
+ call s:Log(' Match for "end" in line ' . lnum_new . ': ' .
+ \string(indtokens[i]))
+ endif
+
+ else " token is one of the following: ')', ']', '}'
+
+ call s:Push(stack, token)
+
+ " We have to escape '[', because this string will be interpreted as a
+ " regexp
+ let open_paren = (token ==# ')' ? '(' :
+ \token ==# ']' ? '\[' :
+ \ '{')
+
+ let [lnum_new, col_new] = s:SearchPair(lnum, curr_col,
+ \open_paren, '', token)
+
+ if lnum_new ==# 0
+ return s:IndentError('Matching token not found',
+ \token, stack)
+ else
+ if lnum_new != lnum
+ call s:Log(' Tokenize the opening paren <<<<')
+ let [lnum, indtokens] = s:TokenizeLine(lnum_new, 'up')
+ call s:Log(' >>>>')
+ endif
+
+ let [success, i] = s:GetIndtokenAtCol(indtokens, col_new)
+ if !success | return i | endif
+ let [token, curr_vcol, curr_col] = indtokens[i]
+ call s:Log(' Match in line ' . lnum_new . ': ' .
+ \string(indtokens[i]))
+
+ " Go back to the beginning of the loop and handle the opening paren
+ continue
+ endif
+ endif
+
+ elseif token ==# ';'
+
+ if empty(stack)
+ call s:Push(stack, ';')
+ elseif index([';', '->', 'when', 'end', 'after', 'catch'],
+ \stack[0]) != -1
+ " Pass:
+ "
+ " - If the stack top is another ';', then one ';' is
+ " enough.
+ " - If the stack top is an '->' or a 'when', then we
+ " should keep that, because they signify the type of the
+ " LTI (branch, condition or guard).
+ " - From the indentation point of view, the keyword
+ " (of/catch/after/end) before the LTI is what counts, so
+ " if the stack already has a catch/after/end, we don't
+ " modify it. This way when we reach case/try/receive,
+ " there will be at most one of/catch/after/end token in
+ " the stack.
+ else
+ return s:UnexpectedToken(token, stack)
+ endif
+
+ elseif token ==# '->'
+
+ if empty(stack) && !last_token_of_line
+ call s:Log(' LTI is in expression after arrow -> return')
+ return stored_vcol
+ elseif empty(stack) || stack[0] ==# ';' || stack[0] ==# 'end'
+ " stack = [';'] -> LTI is either a branch or in a guard
+ " stack = ['->'] -> LTI is a condition
+ " stack = ['->', ';'] -> LTI is a branch
+ call s:Push(stack, '->')
+ elseif index(['->', 'when', 'end', 'after', 'catch'], stack[0]) != -1
+ " Pass:
+ "
+ " - If the stack top is another '->', then one '->' is
+ " enough.
+ " - If the stack top is a 'when', then we should keep
+ " that, because this signifies that LTI is a in a guard.
+ " - From the indentation point of view, the keyword
+ " (of/catch/after/end) before the LTI is what counts, so
+ " if the stack already has a catch/after/end, we don't
+ " modify it. This way when we reach case/try/receive,
+ " there will be at most one of/catch/after/end token in
+ " the stack.
+ else
+ return s:UnexpectedToken(token, stack)
+ endif
+
+ elseif token ==# 'when'
+
+ " Pop all ';' from the top of the stack
+ while !empty(stack) && stack[0] ==# ';'
+ call s:Pop(stack)
+ endwhile
+
+ if empty(stack)
+ if semicolon_abscol != ''
+ let stored_vcol = semicolon_abscol
+ endif
+ if !last_token_of_line
+ " Example:
+ " when A,
+ " LTI
+ let [ret, res] = s:BeginElementFoundIfEmpty(stack, token, curr_vcol,
+ \stored_vcol, shiftwidth())
+ if ret | return res | endif
+ else
+ " Example:
+ " when
+ " LTI
+ call s:Push(stack, token)
+ endif
+ elseif index(['->', 'when', 'end', 'after', 'catch'], stack[0]) != -1
+ " Pass:
+ " - If the stack top is another 'when', then one 'when' is
+ " enough.
+ " - If the stack top is an '->' or a 'when', then we
+ " should keep that, because they signify the type of the
+ " LTI (branch, condition or guard).
+ " - From the indentation point of view, the keyword
+ " (of/catch/after/end) before the LTI is what counts, so
+ " if the stack already has a catch/after/end, we don't
+ " modify it. This way when we reach case/try/receive,
+ " there will be at most one of/catch/after/end token in
+ " the stack.
+ else
+ return s:UnexpectedToken(token, stack)
+ endif
+
+ elseif token ==# 'of' || token ==# 'after' ||
+ \ (token ==# 'catch' && !s:IsCatchStandalone(lnum, i))
+
+ if token ==# 'after'
+ " If LTI is between an 'after' and the corresponding
+ " 'end', then let's return
+ let [ret, res] = s:BeginElementFoundIfEmpty(stack, token, curr_vcol,
+ \stored_vcol, shiftwidth())
+ if ret | return res | endif
+ endif
+
+ if empty(stack) || stack[0] ==# '->' || stack[0] ==# 'when'
+ call s:Push(stack, token)
+ elseif stack[0] ==# 'catch' || stack[0] ==# 'after' || stack[0] ==# 'end'
+ " Pass: From the indentation point of view, the keyword
+ " (of/catch/after/end) before the LTI is what counts, so
+ " if the stack already has a catch/after/end, we don't
+ " modify it. This way when we reach case/try/receive,
+ " there will be at most one of/catch/after/end token in
+ " the stack.
+ else
+ return s:UnexpectedToken(token, stack)
+ endif
+
+ elseif token ==# '||' && empty(stack) && !last_token_of_line
+
+ call s:Log(' LTI is in expression after "||" -> return')
+ return stored_vcol
+
+ else
+ call s:Log(' Misc token, stack unchanged = ' . string(stack))
+
+ endif
+
+ if empty(stack) || stack[0] ==# '->' || stack[0] ==# 'when'
+ let stored_vcol = curr_vcol
+ let semicolon_abscol = ''
+ call s:Log(' Misc token when the stack is empty or has "->" ' .
+ \'-> setting stored_vcol to ' . stored_vcol)
+ elseif stack[0] ==# ';'
+ let semicolon_abscol = curr_vcol
+ call s:Log(' Setting semicolon-stored_vcol to ' . stored_vcol)
+ endif
+
+ let i -= 1
+ call s:Log(' Token processed. stored_vcol=' . stored_vcol)
+
+ let last_token_of_line = 0
+
+ endwhile " iteration on tokens in a line
+
+ call s:Log(' Line analyzed. stored_vcol=' . stored_vcol)
+
+ if empty(stack) && stored_vcol != -1 &&
+ \ (!empty(indtokens) && indtokens[0][0] != '<string_end>' &&
+ \ indtokens[0][0] != '<quoted_atom_end>')
+ call s:Log(' Empty stack at the beginning of the line -> return')
+ return stored_vcol
+ endif
+
+ let lnum -= 1
+
+ endwhile " iteration on lines
+
+endfunction
+
+" ErlangIndent function {{{1
+" =====================
+
+function! ErlangIndent()
+
+ call s:ClearTokenCacheIfNeeded()
+
+ let currline = getline(v:lnum)
+ call s:Log('Indenting line ' . v:lnum . ': ' . currline)
+
+ if s:IsLineStringContinuation(v:lnum) || s:IsLineAtomContinuation(v:lnum)
+ call s:Log('String or atom continuation found -> ' .
+ \'leaving indentation unchanged')
+ return -1
+ endif
+
+ let ml = matchlist(currline,
+ \'^\(\s*\)\(\%(end\|of\|catch\|after\)\>\|[)\]}]\|>>\)')
+
+ " If the line has a special beginning, but not a standalone catch
+ if !empty(ml) && !(ml[2] ==# 'catch' && s:IsCatchStandalone(v:lnum, 0))
+
+ let curr_col = len(ml[1])
+
+ " If we can be sure that there is synchronization in the Erlang
+ " syntax, we use searchpair to make the script quicker.
+ if ml[2] ==# 'end' && exists('b:erlang_syntax_synced')
+
+ let [lnum, col] = s:SearchEndPair(v:lnum, curr_col)
+
+ if lnum ==# 0
+ return s:IndentError('Matching token for "end" not found',
+ \'end', [])
+ else
+ call s:Log(' Tokenize for "end" <<<<')
+ let [lnum, indtokens] = s:TokenizeLine(lnum, 'up')
+ call s:Log(' >>>> Tokenize for "end"')
+
+ let [success, i] = s:GetIndtokenAtCol(indtokens, col)
+ if !success | return i | endif
+ let [token, curr_vcol, curr_col] = indtokens[i]
+ call s:Log(' Match for "end" in line ' . lnum . ': ' .
+ \string(indtokens[i]))
+ return curr_vcol
+ endif
+
+ else
+
+ call s:Log(" Line type = 'end'")
+ let new_col = s:ErlangCalcIndent(v:lnum - 1,
+ \[ml[2], 'align_to_begin_element'])
+ endif
+ else
+ call s:Log(" Line type = 'normal'")
+
+ let new_col = s:ErlangCalcIndent(v:lnum - 1, [])
+ if currline =~# '^\s*when\>'
+ let new_col += 2
+ endif
+ endif
+
+ if new_col < -1
+ call s:Log('WARNING: returning new_col == ' . new_col)
+ return g:erlang_unexpected_token_indent
+ endif
+
+ return new_col
+
+endfunction
+
+" Cleanup {{{1
+" =======
+
+let &cpo = s:cpo_save
+unlet s:cpo_save
+
+" vim: sw=2 et fdm=marker