diff options
author | Bram Moolenaar <Bram@vim.org> | 2013-05-17 18:14:19 +0200 |
---|---|---|
committer | Bram Moolenaar <Bram@vim.org> | 2013-05-17 18:14:19 +0200 |
commit | ad3b366c8250a6f3c7ac699bc4b0f2e613286089 (patch) | |
tree | 23a07a413110c9587852eb5ec0e29119d3debc62 /runtime/indent/erlang.vim | |
parent | d620aa9be4d574627c020dedd39313f8482ab216 (diff) |
Update runtime files.
Diffstat (limited to 'runtime/indent/erlang.vim')
-rw-r--r-- | runtime/indent/erlang.vim | 1500 |
1 files changed, 1331 insertions, 169 deletions
diff --git a/runtime/indent/erlang.vim b/runtime/indent/erlang.vim index 930f5d1325..416c40e935 100644 --- a/runtime/indent/erlang.vim +++ b/runtime/indent/erlang.vim @@ -1,213 +1,1375 @@ " Vim indent file -" Language: Erlang +" 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-Mar-05 " License: Vim license -" Version: 2011/09/06 +" 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 -if exists("b:did_indent") - finish +" Vim 7 or later is needed +if exists("b:did_indent") || version < 700 + finish else - let b:did_indent = 1 + let b:did_indent = 1 endif setlocal indentexpr=ErlangIndent() -setlocal indentkeys+==after,=end,=catch,=),=],=} +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 + finish endif -" The function goes through the whole line, analyses it and returns the -" indentation level. -" -" line: the line to be examined -" return: the indentation level of the examined line -function s:ErlangIndentAfterLine(line) - let linelen = strlen(a:line) " the length of the line - let i = 0 " the index of the current character in the line - let ind = 0 " how much should be the difference between the indentation of - " the current line and the indentation of the next line? - " e.g. +1: the indentation of the next line should be equal to - " the indentation of the current line plus one shiftwidth - let last_fun = 0 " the last token was a 'fun' - let last_receive = 0 " the last token was a 'receive'; needed for 'after' - let last_hash_sym = 0 " the last token was a '#' - - " Ignore comments - if a:line =~# '^\s*%' - return 0 - endif - - " Partial function head where the guard is missing - if a:line =~# "\\(^\\l[[:alnum:]_]*\\)\\|\\(^'[^']\\+'\\)(" && a:line !~# '->' - return 2 - endif - - " The missing guard from the split function head - if a:line =~# '^\s*when\s\+.*->' - return -1 - endif - - while 0<=i && i<linelen - " m: the next value of the i - if a:line[i] == '"' - let m = matchend(a:line,'"\%([^"\\]\|\\.\)*"',i) - let last_receive = 0 - elseif a:line[i] == "'" - let m = matchend(a:line,"'[^']*'",i) - let last_receive = 0 - elseif a:line[i] =~# "[a-z]" - let m = matchend(a:line,".[[:alnum:]_]*",i) - if last_fun - let ind = ind - 1 - let last_fun = 0 - let last_receive = 0 - elseif a:line[(i):(m-1)] =~# '^\%(case\|if\|try\)$' - let ind = ind + 1 - elseif a:line[(i):(m-1)] =~# '^receive$' - let ind = ind + 1 - let last_receive = 1 - elseif a:line[(i):(m-1)] =~# '^begin$' - let ind = ind + 2 - let last_receive = 0 - elseif a:line[(i):(m-1)] =~# '^end$' - let ind = ind - 2 - let last_receive = 0 - elseif a:line[(i):(m-1)] =~# '^after$' - if last_receive == 0 - let ind = ind - 1 - else - let ind = ind + 0 - endif - let last_receive = 0 - elseif a:line[(i):(m-1)] =~# '^fun$' - let ind = ind + 1 - let last_fun = 1 - let last_receive = 0 - endif - elseif a:line[i] =~# "[A-Z_]" - let m = matchend(a:line,".[[:alnum:]_]*",i) - let last_receive = 0 - elseif a:line[i] == '$' - let m = i+2 - let last_receive = 0 - elseif a:line[i] == "." && (i+1>=linelen || a:line[i+1]!~ "[0-9]") - let m = i+1 - if last_hash_sym - let last_hash_sym = 0 - else - let ind = ind - 1 - endif - let last_receive = 0 - elseif a:line[i] == '-' && (i+1<linelen && a:line[i+1]=='>') - let m = i+2 - let ind = ind + 1 - let last_receive = 0 - elseif a:line[i] == ';' && a:line[(i):(linelen)] !~# '.*->.*' - let m = i+1 - let ind = ind - 1 - let last_receive = 0 - elseif a:line[i] == '#' - let m = i+1 - let last_hash_sym = 1 - elseif a:line[i] =~# '[({[]' - let m = i+1 - let ind = ind + 1 - let last_fun = 0 - let last_receive = 0 - let last_hash_sym = 0 - elseif a:line[i] =~# '[)}\]]' - let m = i+1 - let ind = ind - 1 - let last_receive = 0 - else - let m = i+1 - 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 i = m - endwhile + 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 - return ind endfunction -function s:FindPrevNonBlankNonComment(lnum) +" 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) - " Continue to search above if the current line begins with a '%' - while line =~# '^\s*%.*$' - let lnum = prevnonblank(lnum - 1) - if 0 == lnum - return 0 - endif - let line = getline(lnum) - endwhile - return 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 -" The function returns the indentation level of the line adjusted to a mutiple -" of 'shiftwidth' option. +" 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. " -" lnum: line number -" return: the indentation level of the line -function s:GetLineIndent(lnum) - return (indent(a:lnum) / &sw) * &sw +" 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 -function ErlangIndent() - " Find a non-blank line above the current line - let lnum = prevnonblank(v:lnum - 1) +" 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 - " Hit the start of the file, use zero indent - if lnum == 0 - return 0 - endif +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) - let prevline = getline(lnum) - let currline = getline(v:lnum) + " If we hit the beginning of the file, it is not a catch in a try block + if prev_indtoken == [] + return 1 + endif - let ind_after = s:ErlangIndentAfterLine(prevline) - if ind_after != 0 - let ind = s:GetLineIndent(lnum) + ind_after * &sw + 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 - let ind = indent(lnum) + ind_after * &sw + " 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 - " Special cases: - if prevline =~# '^\s*\%(after\|end\)\>' - let ind = ind + 2*&sw +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 - if currline =~# '^\s*end\>' - let ind = ind - 2*&sw + 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 - if currline =~# '^\s*after\>' - let plnum = s:FindPrevNonBlankNonComment(v:lnum-1) - if getline(plnum) =~# '^[^%]*\<receive\>\s*\%(%.*\)\=$' - " If the 'receive' is not in the same line as the 'after' - let ind = ind - 1*&sw - else - let ind = ind - 2*&sw - 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 + &sw + 2] + else + return [1, s:UnexpectedToken(a:token, a:stack)] endif - if prevline =~# '^\s*[)}\]]' - let ind = ind + 1*&sw + 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 + &sw] + 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 - if currline =~# '^\s*[)}\]]' - let ind = ind - 1*&sw + 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, + \ '\<\%(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 - if prevline =~# '^\s*\%(catch\)\s*\%(%\|$\)' - let ind = ind + 1*&sw + + 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', &sw) + 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 + &sw + elseif stack == ['when'] + call s:Log(' LTI is in a guard after ' . + \'"of/receive/after/if/catch" -> return') + return stored_vcol + &sw + 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 |