From 7f2e3663341d725b073857fcd3b2f5868ce85248 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Thu, 11 Dec 2014 22:58:14 +0100 Subject: [PATCH] Change erlang provider to hcs42/vim-erlang-runtime --- README.md | 2 +- autoload/erlangcomplete.vim | 161 ---- build | 2 +- compiler/erlang.vim | 80 -- ftplugin/erlang.vim | 154 ---- ftplugin/erlang_refactor.vim | 295 ------- indent/erlang.vim | 1616 ++++++++++++++++++++++++++++++---- syntax/erlang.vim | 380 +++++--- 8 files changed, 1704 insertions(+), 986 deletions(-) delete mode 100644 autoload/erlangcomplete.vim delete mode 100644 compiler/erlang.vim delete mode 100644 ftplugin/erlang.vim delete mode 100644 ftplugin/erlang_refactor.vim diff --git a/README.md b/README.md index 5a12902..08509ee 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Optionally download one of the [releases](https://github.com/sheerun/vim-polyglo - [elixir](https://github.com/elixir-lang/vim-elixir) (syntax, indent, compiler, ftplugin, ftdetect) - [emberscript](https://github.com/heartsentwined/vim-ember-script) (syntax, indent, ftplugin, ftdetect) - [emblem](https://github.com/heartsentwined/vim-emblem) (syntax, indent, ftplugin, ftdetect) -- [erlang](https://github.com/oscarh/vimerl) (syntax, indent, compiler, autoload, ftplugin) +- [erlang](https://github.com/hcs42/vim-erlang-runtime) (syntax, indent) - [git](https://github.com/tpope/vim-git) (syntax, indent, ftplugin, ftdetect) - [go](https://github.com/fatih/vim-go) (syntax, indent, ftdetect) - [haml](https://github.com/tpope/vim-haml) (syntax, indent, compiler, ftplugin, ftdetect) diff --git a/autoload/erlangcomplete.vim b/autoload/erlangcomplete.vim deleted file mode 100644 index 3e4208e..0000000 --- a/autoload/erlangcomplete.vim +++ /dev/null @@ -1,161 +0,0 @@ -" ------------------------------------------------------------------------------ -" Vim omni-completion script -" Author: Oscar Hellström -" Email: oscar@oscarh.net -" Version: 2010-08-10 -" Contributors: kTT (http://github.com/kTT) -" Ricardo Catalinas Jiménez -" ------------------------------------------------------------------------------ - -" Patterns for completions {{{1 -let s:erlangLocalFuncBeg = '\(\<[0-9A-Za-z_-]*\|\s*\)$' -let s:erlangExternalFuncBeg = '\<[0-9A-Za-z_-]\+:[0-9A-Za-z_-]*$' -let s:ErlangBlankLine = '^\s*\(%.*\)\?$' -let s:erlangCompletionPath = expand(':p:h') . '/erlang_completion.erl' - -if !exists('g:erlangCompletionGrep') - let g:erlangCompletionGrep = 'grep' -endif - -if !exists('g:erlangManSuffix') - let g:erlangManSuffix = '' -endif - -if !exists('g:erlangManPath') - let g:erlangManPath = '/usr/lib/erlang/man' -endif - -if !exists('g:erlangCompletionDisplayDoc') - let g:erlangCompletionDisplayDoc = 1 -endif - -" Main function for completion {{{1 -function! erlangcomplete#Complete(findstart, base) - " 0) Init {{{2 - let lnum = line('.') - let column = col('.') - let line = strpart(getline('.'), 0, column - 1) - - " 1) First, check if completion is impossible {{{2 - if line =~ '[^~\\]%' - return -1 - endif - - "echo "line[col - 1]:" . line[column - 1] . " line[col - 2]:" . line[column - 2] . "\n" . line . "\n" - - " 2) Check if the char to the left of us are part of a function call {{{2 - " - " Nothing interesting is written at the char just before the cursor - " This means _anything_ could be started here - " In this case, keyword completion should probably be used, - " for now we'll only try and complete local functions. - " TODO: Examine if we can stare Identifiers end complete on them - " Is this worth it? Is /completion/ of a "blank" wanted? Can we consider ( - " interesting and check if we are in a function call etc.? - if line[column - 2] !~ '[0-9A-Za-z:_-]' - if a:findstart - return column - else - return s:erlangFindLocalFunc(a:base) - endif - endif - - - " 3) Function in external module {{{2 - if line =~ s:erlangExternalFuncBeg - let delimiter = match(line, ':[0-9A-Za-z_-]*$') + 1 - if a:findstart - return delimiter - else - let module = matchstr(line[:-2], '\<\k*\>$') - return s:erlangFindExternalFunc(module, a:base) - endif - endif - - " 4) Local function {{{2 - if line =~ s:erlangLocalFuncBeg - let funcstart = match(line, ':\@/dev/null' '2>/dev/null' - redraw! - endif - let functions = system(s:erlangCompletionPath . ' ' . a:module) - for element in sort(split(functions, '\n')) - if match(element, a:base) == 0 - let function_name = matchstr(element, a:base . '\w\+') - let number_of_args = matchstr(element, '\d\+', len(function_name)) - let number_of_comma = max([number_of_args - 1, 0]) - let file_path = g:erlangManPath . '/man?/' . a:module . '\.?' . g:erlangManSuffix - " [:-2] cutting some weird characters at the end - " becouse grep doesn't support multilines, we have to filter - " first by .B and next by looking via function name - " if someone have better idea, please change it - let description = '' - " Don't look man pages if the module is present in the current directory - if g:erlangCompletionDisplayDoc != 0 && !filereadable(a:module . '.erl') - let system_command = g:erlangCompletionGrep . ' -A 1 "\.B" ' . file_path . ' | grep -EZo "\<' . -\ function_name . '\>\((\w+, ){' . number_of_comma . '}[^),]*\) -> .*"' - let description = system(system_command) - let description = description[:-2] - endif - if description == '' - let description = element " if function doesn't have description e.g. lists:rmerge, put rmerge/2 instead - endif - let field = {'word': function_name . '(', 'abbr': description, 'kind': 'f', 'dup': 1} " always duplicate functions - call complete_add(field) - endif - endfor - return [] -endfunction - -" Find local function names {{{2 -function s:erlangFindLocalFunc(base) - " begin at line 1 - let lnum = s:erlangFindNextNonBlank(1) - if "" == a:base - let base = '\w' " used to match against word symbol - else - let base = a:base - endif - while 0 != lnum && !complete_check() - let line = getline(lnum) - let function_name = matchstr(line, '^' . base . '[0-9A-Za-z_-]\+(\@=') - if function_name != "" - call complete_add(function_name) - endif - let lnum = s:erlangFindNextNonBlank(lnum) - endwhile - return [] -endfunction - diff --git a/build b/build index ba798d5..40927cb 100755 --- a/build +++ b/build @@ -82,7 +82,7 @@ PACKS=" elixir:elixir-lang/vim-elixir emberscript:heartsentwined/vim-ember-script emblem:heartsentwined/vim-emblem - erlang:oscarh/vimerl + erlang:hcs42/vim-erlang-runtime git:tpope/vim-git go:fatih/vim-go:_BASIC haml:tpope/vim-haml diff --git a/compiler/erlang.vim b/compiler/erlang.vim deleted file mode 100644 index 6ab7526..0000000 --- a/compiler/erlang.vim +++ /dev/null @@ -1,80 +0,0 @@ -" Erlang compiler file -" Language: Erlang -" Maintainer: Pawel 'kTT' Salata -" URL: http://ktototaki.info - -if exists("current_compiler") - finish -endif -let current_compiler = "erlang" - -if exists(":CompilerSet") != 2 - command -nargs=* CompilerSet setlocal -endif - -if !exists('g:erlangCheckFile') - let g:erlangCheckFile = "~/.vim/compiler/erlang_check_file.erl" -endif - -if !exists('g:erlangHighlightErrors') - let g:erlangHighlightErrors = 0 -endif - -let b:error_list = {} -let b:is_showing_msg = 0 - -function! HighlightErlangErrors() - if match(getline(1), "#!.*escript") != -1 - setlocal makeprg=escript\ -s\ % - else - execute "setlocal makeprg=" . g:erlangCheckFile . "\\ \%" - endif - silent make! - call s:clear_matches() - for error in getqflist() - let item = {} - let item['lnum'] = error.lnum - let item['msg'] = error.text - let b:error_list[error.lnum] = item - call matchadd('SpellBad', "\\%" . error.lnum . "l") - endfor - if len(getqflist()) - redraw! - endif - call s:show_msg() - setlocal makeprg=erlc\ % -endfunction - -function! s:show_msg() - let pos = getpos(".") - if has_key(b:error_list, pos[1]) - let item = get(b:error_list, pos[1]) - echo item.msg - let b:is_showing_msg = 1 - else - if exists("b:is_showing_msg") && b:is_showing_msg == 1 - echo - let b:is_showing_msg = 0 - endif - endif -endf - -function! s:clear_matches() - call clearmatches() - let b:error_list = {} - if exists("b:is_showing_msg") && b:is_showing_msg == 1 - echo - let b:is_showing_msg = 0 - endif -endfunction - -CompilerSet makeprg=erlc\ % -CompilerSet errorformat=%f:%l:\ %tarning:\ %m,%E%f:%l:\ %m - -if g:erlangHighlightErrors - autocmd BufLeave *.erl call s:clear_matches() - autocmd BufEnter *.erl call s:clear_matches() - autocmd BufWritePost *.erl call HighlightErlangErrors() - autocmd CursorHold *.erl call s:show_msg() - autocmd CursorMoved *.erl call s:show_msg() -endif diff --git a/ftplugin/erlang.vim b/ftplugin/erlang.vim deleted file mode 100644 index 4f1acfe..0000000 --- a/ftplugin/erlang.vim +++ /dev/null @@ -1,154 +0,0 @@ -" Vim ftplugin file -" Language: Erlang -" Maintainer: Oscar Hellström -" URL: http://personal.oscarh.net -" Contributor: Ricardo Catalinas Jiménez -" Version: 2010-09-03 -" ------------------------------------------------------------------------------ -" Usage: -" -" To enable folding put in your vimrc: -" set foldenable -" -" Folding will make only one fold for a complete function, even though it has -" more than one function head and body. -" -" To change this behaviour put in your vimrc file: -" let g:erlangFoldSplitFunction=1 -" -" ------------------------------------------------------------------------------ -" Plugin init -if exists("b:did_ftplugin") - finish -endif - -" Don't load any other -let b:did_ftplugin=1 - -if exists('s:doneFunctionDefinitions') - call s:SetErlangOptions() - finish -endif - -let s:doneFunctionDefinitions=1 - -" Local settings -function s:SetErlangOptions() - compiler erlang - if version >= 700 - setlocal omnifunc=erlangcomplete#Complete - endif - - setlocal comments=:%%%,:%%,:% - setlocal commentstring=%%s - - setlocal foldmethod=expr - setlocal foldexpr=GetErlangFold(v:lnum) - setlocal foldtext=ErlangFoldText() -endfunction - -" Define folding functions -if !exists("*GetErlangFold") - " Folding params - let s:ErlangFunBegin = '^\a\w*(.*$' - let s:ErlangFunEnd = '^[^%]*\.\s*\(%.*\)\?$' - let s:ErlangBlankLine = '^\s*\(%.*\)\?$' - - " Auxiliary fold functions - function s:GetNextNonBlank(lnum) - let lnum = nextnonblank(a:lnum + 1) - let line = getline(lnum) - while line =~ s:ErlangBlankLine && 0 != lnum - let lnum = nextnonblank(lnum + 1) - let line = getline(lnum) - endwhile - return lnum - endfunction - - function s:GetFunName(str) - return matchstr(a:str, '^\a\w*(\@=') - endfunction - - function s:GetFunArgs(str, lnum) - let str = a:str - let lnum = a:lnum - while str !~ '->\s*\(%.*\)\?$' - let lnum = s:GetNextNonBlank(lnum) - if 0 == lnum " EOF - return "" - endif - let str .= getline(lnum) - endwhile - return matchstr(str, - \ '\(^(\s*\)\@<=.*\(\s*)\(\s\+when\s\+.*\)\?\s\+->\s*\(%.*\)\?$\)\@=') - endfunction - - function s:CountFunArgs(arguments) - let pos = 0 - let ac = 0 " arg count - let arguments = a:arguments - - " Change list / tuples into just one A(rgument) - let erlangTuple = '{\([A-Za-z_,|=\-\[\]]\|\s\)*}' - let erlangList = '\[\([A-Za-z_,|=\-{}]\|\s\)*\]' - - " FIXME: Use searchpair? - while arguments =~ erlangTuple - let arguments = substitute(arguments, erlangTuple, "A", "g") - endwhile - " FIXME: Use searchpair? - while arguments =~ erlangList - let arguments = substitute(arguments, erlangList, "A", "g") - endwhile - - let len = strlen(arguments) - while pos < len && pos > -1 - let ac += 1 - let pos = matchend(arguments, ',\s*', pos) - endwhile - return ac - endfunction - - " Main fold function - function GetErlangFold(lnum) - let lnum = a:lnum - let line = getline(lnum) - - if line =~ s:ErlangFunEnd - return '<1' - endif - - if line =~ s:ErlangFunBegin && foldlevel(lnum - 1) == 1 - if exists("g:erlangFoldSplitFunction") && g:erlangFoldSplitFunction - return '>1' - else - return '1' - endif - endif - - if line =~ s:ErlangFunBegin - return '>1' - endif - - return '=' - endfunction - - " Erlang fold description (foldtext function) - function ErlangFoldText() - let foldlen = v:foldend - v:foldstart - if 1 < foldlen - let lines = "lines" - else - let lines = "line" - endif - let line = getline(v:foldstart) - let name = s:GetFunName(line) - let arguments = s:GetFunArgs(strpart(line, strlen(name)), v:foldstart) - let argcount = s:CountFunArgs(arguments) - let retval = "+" . v:folddashes . " " . name . "/" . argcount - let retval .= " (" . foldlen . " " . lines . ")" - return retval - endfunction -endif - -call s:SetErlangOptions() diff --git a/ftplugin/erlang_refactor.vim b/ftplugin/erlang_refactor.vim deleted file mode 100644 index f809db9..0000000 --- a/ftplugin/erlang_refactor.vim +++ /dev/null @@ -1,295 +0,0 @@ -" Erlang refactor file -" Language: Erlang -" Maintainer: Pawel 'kTT' Salata -" URL: http://ktototaki.info - -if exists("b:did_ftplugin_erlang") - finish -endif - -" Don't load any other -let b:did_ftplugin_erlang=1 - -if !exists('g:erlangRefactoring') || g:erlangRefactoring == 0 - finish -endif - -if !exists('g:erlangWranglerPath') - let g:erlangWranglerPath = '/usr/share/wrangler/' -endif - -if glob(g:erlangWranglerPath) == "" - call confirm("Wrong path to wrangler dir") - finish -endif - -autocmd VimLeavePre * call StopWranglerServer() - -let s:erlangServerName = "wrangler_vim" - -" Starting background erlang session with wrangler on -function! StartWranglerServer() - let wranglerEbinDir = g:erlangWranglerPath . "/ebin" - let command = "erl_call -s -sname " . s:erlangServerName . " -x 'erl -pa " . wranglerEbinDir . "'" - call system(command) - call s:send_rpc('application', 'start', '[wrangler_app]') -endfunction - -" Stopping erlang session -function! StopWranglerServer() - echo s:send_rpc('erlang', 'halt', '') -endfunction - -" Sending rpc call to erlang session -function! s:send_rpc(module, fun, args) - let command = "erl_call -sname " . s:erlangServerName . " -a '" . a:module . " " . a:fun . " " . a:args . "'" - let result = system(command) - if match(result, 'erl_call: failed to connect to node .*') != -1 - call StartWranglerServer() - return system(command) - endif - return result -endfunction - -function! ErlangUndo() - echo s:send_rpc("wrangler_undo_server", "undo", "[]") - :e! -endfunction - -function! s:trim(text) - return substitute(a:text, "^\\s\\+\\|\\s\\+$", "", "g") -endfunction - -function! s:get_msg(result, tuple_start) - let msg_begin = '{' . a:tuple_start . ',' - let matching_start = match(a:result, msg_begin) - if matching_start != -1 - return s:trim(matchstr(a:result, '[^}]*', matching_start + strlen(msg_begin))) - endif - return "" -endfunction - -" Check if there is an error in result -function! s:check_for_error(result) - let msg = s:get_msg(a:result, 'ok') - if msg != "" - return [0, msg] - endif - let msg = s:get_msg(a:result, 'warning') - if msg != "" - return [1, msg] - endif - let msg = s:get_msg(a:result, 'error') - if msg != "" - return [2, msg] - endif - return [-1, ""] -endfunction - -" Sending apply changes to file -function! s:send_confirm() - let choice = confirm("What do you want?", "&Preview\n&Confirm\nCa&ncel", 0) - if choice == 1 - echo "TODO: Display preview :)" - elseif choice == 2 - let module = 'wrangler_preview_server' - let fun = 'commit' - let args = '[]' - return s:send_rpc(module, fun, args) - else - let module = 'wrangler_preview_server' - let fun = 'abort' - let args = '[]' - return s:send_rpc(module, fun, args) - echo "Canceled" - endif -endfunction - -" Manually send confirm, for testing purpose only -function! SendConfirm() - echo s:send_confirm() -endfunction - -" Format and send function extracton call -function! s:call_extract(start_line, start_col, end_line, end_col, name) - let file = expand("%:p") - let module = 'wrangler' - let fun = 'fun_extraction' - let args = '["' . file . '", {' . a:start_line . ', ' . a:start_col . '}, {' . a:end_line . ', ' . a:end_col . '}, "' . a:name . '", ' . &sw . ']' - let result = s:send_rpc(module, fun, args) - let [error_code, msg] = s:check_for_error(result) - if error_code != 0 - call confirm(msg) - return 0 - endif - echo "This files will be changed: " . matchstr(msg, "[^]]*", 1) - echo s:send_confirm() - return 1 -endfunction - -function! ErlangExtractFunction(mode) range - silent w! - let name = inputdialog("New function name: ") - if name != "" - if a:mode == "v" - let start_pos = getpos("'<") - let start_line = start_pos[1] - let start_col = start_pos[2] - - let end_pos = getpos("'>") - let end_line = end_pos[1] - let end_col = end_pos[2] - elseif a:mode == "n" - let pos = getpos(".") - let start_line = pos[1] - let start_col = pos[2] - let end_line = pos[1] - let end_col = pos[2] - else - echo "Mode not supported." - return - endif - if s:call_extract(start_line, start_col, end_line, end_col, name) - let temp = &autoread - set autoread - :e - if temp == 0 - set noautoread - endif - endif - else - echo "Empty function name. Ignoring." - endif -endfunction -nmap e :call ErlangExtractFunction("n") -vmap e :call ErlangExtractFunction("v") - -function! s:call_rename(mode, line, col, name, search_path) - let file = expand("%:p") - let module = 'wrangler' - let fun = 'rename_' . a:mode - let args = '["' . file .'", ' - if a:mode != "mod" - let args = args . a:line . ', ' . a:col . ', ' - endif - let args = args . '"' . a:name . '", ["' . a:search_path . '"], ' . &sw . ']' - let result = s:send_rpc(module, fun, args) - let [error_code, msg] = s:check_for_error(result) - if error_code != 0 - call confirm(msg) - return 0 - endif - echo "This files will be changed: " . matchstr(msg, "[^]]*", 1) - echo s:send_confirm() - return 1 -endfunction - -function! ErlangRename(mode) - silent w! - if a:mode == "mod" - let name = inputdialog('Rename module to: ') - else - let name = inputdialog('Rename "' . expand("") . '" to: ') - endif - if name != "" - let search_path = expand("%:p:h") - "let search_path = inputdialog('Search path: ', expand("%:p:h")) - let pos = getpos(".") - let line = pos[1] - let col = pos[2] - let current_filename = expand("%") - let current_filepath = expand("%:p") - let new_filename = name . '.erl' - if s:call_rename(a:mode, line, col, name, search_path) - if a:mode == "mod" - execute ':bd ' . current_filename - execute ':e ' . new_filename - silent execute '!mv ' . current_filepath . ' ' . current_filepath . '.bak' - redraw! - else - let temp = &autoread - set autoread - :e - if temp == 0 - set noautoread - endif - endif - endif - else - echo "Empty name. Ignoring." - endif -endfunction - -function! ErlangRenameFunction() - call ErlangRename("fun") -endfunction -map f :call ErlangRenameFunction() - -function! ErlangRenameVariable() - call ErlangRename("var") -endfunction -map v :call ErlangRenameVariable() - -function! ErlangRenameModule() - call ErlangRename("mod") -endfunction -map m :call ErlangRenameModule() - -function! ErlangRenameProcess() - call ErlangRename("process") -endfunction -map p :call ErlangRenameProcess() - -function! s:call_tuple_fun_args(start_line, start_col, end_line, end_col, search_path) - let file = expand("%:p") - let module = 'wrangler' - let fun = 'tuple_funpar' - let args = '["' . file . '", {' . a:start_line . ', ' . a:start_col . '}, {' . a:end_line . ', ' . a:end_col . '}, ["' . a:search_path . '"], ' . &sw . ']' - let result = s:send_rpc(module, fun, args) - if s:check_for_error(result) - return 0 - endif - call s:send_confirm() - return 1 -endfunction - -function! ErlangTupleFunArgs(mode) - silent w! - let search_path = expand("%:p:h") - "let search_path = inputdialog('Search path: ', expand("%:p:h")) - if a:mode == "v" - let start_pos = getpos("'<") - let start_line = start_pos[1] - let start_col = start_pos[2] - - let end_pos = getpos("'>") - let end_line = end_pos[1] - let end_col = end_pos[2] - if s:call_tuple_fun_args(start_line, start_col, end_line, end_col, search_path) - let temp = &autoread - set autoread - :e - if temp == 0 - set noautoread - endif - endif - elseif a:mode == "n" - let pos = getpos(".") - let line = pos[1] - let col = pos[2] - if s:call_tuple_fun_args(line, col, line, col, search_path) - let temp = &autoread - set autoread - :e - if temp == 0 - set noautoread - endif - endif - else - echo "Mode not supported." - endif -endfunction -nmap t :call ErlangTupleFunArgs("n") -vmap t :call ErlangTupleFunArgs("v") - -" vim: set foldmethod=marker: diff --git a/indent/erlang.vim b/indent/erlang.vim index 61833f7..6bcec85 100644 --- a/indent/erlang.vim +++ b/indent/erlang.vim @@ -1,207 +1,1481 @@ " Vim indent file -" Language: Erlang -" Maintainer: Csaba Hoch -" Contributor: Edwin Fine -" Contributor: Pawel 'kTT' Salata -" Last Change: 2010 Aug 30 +" Language: Erlang (http://www.erlang.org) +" Author: Csaba Hoch +" Contributors: Edwin Fine +" Pawel 'kTT' Salata +" Ricardo Catalinas Jiménez +" Last Update: 2013-Jul-21 +" License: Vim license +" URL: https://github.com/hcs42/vim-erlang -" Only load this indent file when no other was loaded. -if exists("b:did_indent") +" 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 -let b:did_indent = 1 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. +" Only define the functions once if exists("*ErlangIndent") - finish + finish endif -" The function go through the whole line, analyses it and sets the indentation -" (ind variable). -" l: the number of the line to be examined. -function s:ErlangIndentAfterLine(l) - let i = 0 " the index of the current character in the line - let length = strlen(a:l) " the length of 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 lastFun = 0 " the last token was a 'fun' - let lastReceive = 0 " the last token was a 'receive'; needed for 'after' - let lastHashMark = 0 " the last token was a 'hashmark' +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". See their exact format in the +" documentaiton of the s:GetTokensFromLine function. + +" 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', '', '}') +" vcol = integer (the virtual column of the first character of the token; +" counting starts from 0) +" col = integer (counting starts from 0) +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, ['', 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, ['', 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, ['', vcol, i]) + else + let next_vcol = s:CalcVCol(a:line, i, next_i - 1, vcol, a:tabstop) + call add(indtokens, ['', 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, ['', vcol, i]) + else + let next_vcol = s:CalcVCol(a:line, i, next_i - 1, vcol, a:tabstop) + call add(indtokens, ['', 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: $ (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, ['', 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 - " ignore type annotation lines - if a:l =~# '^\s*-type' - return 0 endif - while 0<= i && i < length + if next_vcol ==# '' + let vcol += next_i - i + else + let vcol = next_vcol + endif - " m: the next value of the i - if a:l[i] == '%' - break - elseif a:l[i] == '"' - let m = matchend(a:l,'"\%([^"\\]\|\\.\)*"',i) - let lastReceive = 0 - elseif a:l[i] == "'" - let m = matchend(a:l,"'[^']*'",i) - let lastReceive = 0 - elseif a:l[i] =~# "[a-z]" - let m = matchend(a:l,".[[:alnum:]_]*",i) - if lastFun - let ind = ind - 1 - let lastFun = 0 - let lastReceive = 0 - elseif a:l[(i):(m-1)] =~# '^\%(case\|if\|try\)$' - let ind = ind + 1 - elseif a:l[(i):(m-1)] =~# '^receive$' - let ind = ind + 1 - let lastReceive = 1 - elseif a:l[(i):(m-1)] =~# '^begin$' - let ind = ind + 2 - let lastReceive = 0 - elseif a:l[(i):(m-1)] =~# '^end$' - let ind = ind - 2 - let lastReceive = 0 - elseif a:l[(i):(m-1)] =~# '^after$' - if lastReceive == 0 - let ind = ind - 1 - else - let ind = ind + 0 - endif - let lastReceive = 0 - elseif a:l[(i):(m-1)] =~# '^fun$' - let ind = ind + 1 - let lastFun = 1 - let lastReceive = 0 - endif - elseif a:l[i] =~# "[A-Z_]" - let m = matchend(a:l,".[[:alnum:]_]*",i) - let lastReceive = 0 - elseif a:l[i] == '$' - let m = i+2 - let lastReceive = 0 - elseif a:l[i] == "." && (i+1>=length || a:l[i+1]!~ "[0-9]") - let m = i+1 - if lastHashMark - let lastHashMark = 0 - else - let ind = ind - 1 - endif - let lastReceive = 0 - elseif a:l[i] == '-' && (i+1') - let m = i+2 - let ind = ind + 1 - let lastReceive = 0 - elseif a:l[i] == ';' && a:l[(i):(length)] !~# '.*->.*' - let m = i+1 - let ind = ind - 1 - let lastReceive = 0 - elseif a:l[i] == '#' - let m = i+1 - let lastHashMark = 1 - elseif a:l[i] =~# '[({[]' - let m = i+1 - let ind = ind + 1 - let lastFun = 0 - let lastReceive = 0 - let lastHashMark = 0 - elseif a:l[i] =~# '[)}\]]' - let m = i+1 - let ind = ind - 1 - let lastReceive = 0 - else - let m = i+1 - endif + let i = next_i - let i = m + endwhile - endwhile - - return ind + return indtokens 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 -function ErlangIndent() +" 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: [[], 0, 0] +" -- the result is an empty list if we hit the beginning or end of +" the file +" | [indtoken, lnum, i] +" -- the content, lnum and token index of the next (or previous) +" 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 [[], 0, 0] + elseif !empty(indtokens) + " We found a non-empty line. If we were moving up, we return the last + " token of this line. Otherwise we return the first token if this line. + let i = (a:dir ==# 'up' ? len(indtokens) - 1 : 0) + return [indtokens[i], lnum, i] + endif + endwhile +endfunction - " Find a non-blank line above the current line. - let lnum = prevnonblank(v:lnum - 1) +" 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) - " Hit the start of the file, use zero indent. - if lnum == 0 - return 0 + " If the current line has a previous token, return that + if a:i > 0 + return [s:all_tokens[a:lnum][a:i - 1], 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], 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([')', ']', '}', '', '', '', + \ '', '$.'], 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 +" lnum: the line number of the "end of clause" mark (or 0 if we hit the +" beginning of the file) +" i: the index of the "end of clause" token within its own line +" 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, lnum, i) + 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 + 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) + return [1, s:UnexpectedToken(a:token, a:stack)] + endif + + if a:lnum ==# 0 + " Set lnum and i to be NextIndToken-friendly + let lnum = 1 + let i = -1 + else + let lnum = a:lnum + let i = a:i + endif + + " Are we after a "-spec func() ...;" clause? + let [next1_indtoken, next1_lnum, next1_i] = s:NextIndToken(lnum, i) + if !empty(next1_indtoken) && next1_indtoken[0] =~# '-' + let [next2_indtoken, next2_lnum, next2_i] = + \s:NextIndToken(next1_lnum, next1_i) + if !empty(next2_indtoken) && next2_indtoken[0] =~# 'spec' + let [next3_indtoken, next3_lnum, next3_i] = + \s:NextIndToken(next2_lnum, next2_i) + if !empty(next3_indtoken) + let [next4_indtoken, next4_lnum, next4_i] = + \s:NextIndToken(next3_lnum, next3_i) + if !empty(next4_indtoken) + " Yes, we are. + call s:Log(' Stack is ["->", ";"], so LTI is in a "-spec" ' . + \'attribute -> return') + return [1, next4_indtoken[1]] + endif + endif + endif + endif + + call s:Log(' Stack is ["->", ";"], so LTI is in a function head ' . + \'-> return') + return [1, a:stored_vcol] + + 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\)\>\|' . + \ '\\%(\s\|\n\|%.*$\|[A-Z_@][a-zA-Z_@]*\)*(', + \ '', + \ '\') +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, 0, 0) + if ret | return res | endif + + return 0 endif - let prevline = getline(lnum) - let currline = getline(v:lnum) + let i = len(indtokens) - 1 + let last_token_of_line = 1 - let ind = indent(lnum) + &sw * s:ErlangIndentAfterLine(prevline) + while i >= 0 - " special cases: - if prevline =~# '^\s*\%(after\|end\)\>' - let ind = ind + 2*&sw - endif - if currline =~# '^\s*end\>' - let ind = ind - 2*&sw - endif - if currline =~# '^\s*after\>' - let plnum = s:FindPrevNonBlankNonComment(v:lnum-1) - if getline(plnum) =~# '^[^%]*\\s*\%(%.*\)\=$' - let ind = ind - 1*&sw - " If the 'receive' is not in the same line as the 'after' + 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 ==# '' + let [ret, res] = s:BeginningOfClauseFound(stack, token, stored_vcol, + \lnum, i) + if ret | return res | endif + + if stored_vcol ==# -1 + call s:Log(' End of clause directly preceeds LTI -> return') + return 0 else - let ind = ind - 2*&sw + call s:Log(' End of clause (but not end of line) -> return') + return stored_vcol endif - endif - if prevline =~# '^\s*[)}\]]' - let ind = ind + 1*&sw - endif - if currline =~# '^\s*[)}\]]' - let ind = ind - 1*&sw - endif - if prevline =~# '^\s*\%(catch\)\s*\%(%\|$\)' - let ind = ind + 1*&sw - endif - if currline =~# '^\s*\%(catch\)\s*\%(%\|$\)' - let ind = ind - 1*&sw + + elseif stack == ['prev_term_plus'] + if token =~# '[a-zA-Z_@]' || + \ token ==# '' || token ==# '' || + \ token ==# '' || token ==# '' + 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 + " 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 + &sw + 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 + &sw + 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 * &sw + 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 * &sw + 2 + endif + + endif + + let [ret, res] = s:BeginElementFound(stack, token, curr_vcol, + \stored_vcol, 'end', &sw) + if ret | return res | endif + + elseif token ==# 'fun' + let [next_indtoken, next_lnum, next_i] = s:NextIndToken(lnum, i) + call s:Log(' Next indtoken = ' . string(next_indtoken)) + + if !empty(next_indtoken) && next_indtoken[0] =~# '^[A-Z_@]' + " The "fun" is followed by a variable, so we might have a named fun: + " "fun Fun() -> ok end". Thus we take the next token to decide + " whether this is a function definition ("fun()") or just a function + " reference ("fun Mod:Fun"). + let [next_indtoken, _, _] = s:NextIndToken(next_lnum, next_i) + call s:Log(' Next indtoken = ' . string(next_indtoken)) + endif + + 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 + &sw + 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 * &sw + 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 * &sw + 2 + endif + + let [ret, res] = s:BeginElementFound(stack, token, curr_vcol, + \stored_vcol, 'end', &sw) + 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, &sw) + 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, &sw) + 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] != '' && + \ indtokens[0][0] != '') + call s:Log(' Empty stack at the beginning of the line -> return') + return stored_vcol endif - if ind<0 - let ind = 0 - endif - return ind + let lnum -= 1 + + endwhile " iteration on lines endfunction -" TODO: -" -" f() -> -" x("foo -" bar") -" , -" bad_indent. -" -" fun -" init/0, -" bad_indent -" -" #rec -" .field, -" bad_indent +" 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 + + " If the line starts with the comment, and so is the previous non-blank line + if currline =~# '^\s*%' + let lnum = prevnonblank(v:lnum - 1) + if lnum ==# 0 + call s:Log('First non-empty line of the file -> return 0.') + return 0 + else + let ml = matchlist(getline(lnum), '^\(\s*\)%') + " If the previous line also starts with a comment, then return the same + " indentation that line has. Otherwise exit from this special "if" and + " don't care that the current line is a comment. + if !empty(ml) + let new_col = s:CalcVCol(ml[1], 0, len(ml[1]) - 1, 0, &tabstop) + call s:Log('Comment line after another comment line -> ' . + \'use same indent: ' . new_col) + return new_col + endif + endif + 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 + +" ErlangShowTokensInLine functions {{{1 +" ================================ + +" These functions are useful during development. + +function! ErlangShowTokensInLine(line) + echo "Line: " . a:line + let indtokens = s:GetTokensFromLine(a:line, 0, 0, &tabstop) + echo "Tokens:" + for it in indtokens + echo it + endfor +endfunction + +function! ErlangShowTokensInCurrentLine() + return ErlangShowTokensInLine(getline('.')) +endfunction + +" Cleanup {{{1 +" ======= + +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 et fdm=marker diff --git a/syntax/erlang.vim b/syntax/erlang.vim index 8b6ad62..7f88355 100644 --- a/syntax/erlang.vim +++ b/syntax/erlang.vim @@ -1,137 +1,271 @@ " Vim syntax file -" Language: Erlang -" Maintainer: Oscar Hellström -" URL: http://oscar.hellstrom.st -" Version: 2010-08-09 -" ------------------------------------------------------------------------------ -" {{{1 -" Options: -" -" Erlang BIFs -" g:erlangHighlightBif - highlight erlang built in functions (default: off) -" -" }}} -" ----------------------------------------------------------------------------- +" Language: Erlang (http://www.erlang.org) +" Maintainer: Csaba Hoch +" Contributor: Adam Rutkowski +" Last Update: 2013-Nov-23 +" License: Vim license +" URL: https://github.com/hcs42/vim-erlang + +" Acknowledgements: This script was originally created by Kresimir Marzic [1]. +" The script was then revamped by Csaba Hoch [2]. During the revamp, the new +" highlighting style and some code was taken from the Erlang syntax script +" that is part of vimerl [3], created by Oscar Hellström [4] and improved by +" Ricardo Catalinas Jiménez [5]. + +" [1]: KreÄ…imir Marľić (Kresimir Marzic) +" [2]: Csaba Hoch +" [3]: https://github.com/jimenezrick/vimerl +" [4]: Oscar Hellström (http://oscar.hellstrom.st) +" [5]: Ricardo Catalinas Jiménez + +" Customization: +" +" To use the old highlighting style, add this to your .vimrc: +" +" let g:erlang_old_style_highlight = 1 +" +" To highlight further module attributes, add them to +" ~/.vim/after/syntax/erlang.vim: +" +" syn keyword erlangAttribute myattr1 myattr2 contained -" Setup {{{1 " For version 5.x: Clear all syntax items " For version 6.x: Quit when a syntax file was already loaded if version < 600 - syntax clear + syntax clear elseif exists("b:current_syntax") - finish + finish endif -" Erlang is case sensitive +let s:cpo_save = &cpo +set cpo&vim + +" Case sensitive syn case match -" Match groups {{{1 -syn match erlangStringModifier /\\./ contained -syn match erlangStringModifier /\~\%(-\?[0-9*]\+\)\?\%(\.[0-9*]\+\..\?\)\?\%(c\|f\|e\|g\|s\|w\|p\|W\|P\|B\|X\|#\|b\|+\|n\|i\)/ contained -syn match erlangModifier /\$\\\?./ - -syn match erlangInteger /\<\%([0-9]\+#[0-9a-fA-F]\+\|[0-9]\+\)\>/ -syn match erlangFloat /\<[0-9]\+\.[0-9]\+\%(e-\?[0-9]\+\)\?\>/ - -syn keyword erlangTodo TODO FIXME XXX contained -syn match erlangComment /%.*$/ contains=@Spell,erlangTodo - -syn keyword erlangKeyword band bor bnot bsl bsr bxor div rem xor -syn keyword erlangKeyword try catch begin receive after cond fun let query - -syn keyword erlangConditional case if of end -syn keyword erlangConditional not and or andalso orelse -syn keyword erlangConditional when - -syn keyword erlangBoolean true false - -syn keyword erlangGuard is_list is_alive is_atom is_binary is_bitstring is_boolean is_tuple is_number is_integer is_float is_function is_constant is_pid is_port is_reference is_record is_process_alive - -syn match erlangOperator /\/\|*\|+\|-\|++\|--/ -syn match erlangOperator /->\|<-\|||\||\|!\|=/ -syn match erlangOperator /=:=\|==\|\/=\|=\/=\|<\|>\|=<\|>=/ -syn keyword erlangOperator div rem - -syn region erlangString start=/"/ end=/"/ skip=/\\/ contains=@Spell,erlangStringModifier - -syn match erlangVariable /\<[A-Z_]\w*\>/ -syn match erlangAtom /\%(\%(^-\)\|#\)\@\%(\s*[(:]\)\@!/ -syn match erlangAtom /\\\@/ -syn match erlangBitSize /:\@<=[0-9]\+/ - -syn match erlangBinary /<<\|>>/ - -" BIFS -syn match erlangBIF /\%([^:0-9A-Za-z_]\|\= 600 + setlocal iskeyword+=$,@-@ +endif + +" Comments +syn match erlangComment '%.*$' contains=erlangCommentAnnotation,erlangTodo +syn match erlangCommentAnnotation ' \@<=@\%(clear\|docfile\|end\|headerfile\|todo\|TODO\|type\|author\|copyright\|doc\|reference\|see\|since\|title\|version\|deprecated\|hidden\|private\|equiv\|spec\|throws\)' contained +syn match erlangCommentAnnotation /`[^']*'/ contained +syn keyword erlangTodo TODO FIXME XXX contained + +" Numbers (minimum base is 2, maximum is 36.) +syn match erlangNumberInteger '\<\d\+\>' +syn match erlangNumberInteger '\<\%([2-9]\|[12]\d\|3[0-6]\)\+#[[:alnum:]]\+\>' +syn match erlangNumberFloat '\<\d\+\.\d\+\%([eE][+-]\=\d\+\)\=\>' + +" Strings, atoms, characters +syn region erlangString start=/"/ end=/"/ contains=erlangStringModifier +syn region erlangQuotedAtom start=/'/ end=/'/ contains=erlangQuotedAtomModifier +syn match erlangStringModifier '\\\%(\o\{1,3}\|x\x\x\|x{\x\+}\|\^.\|.\)\|\~\%([ni~]\|\%(-\=\d\+\|\*\)\=\.\=\%(\*\|\d\+\)\=\%(\..\)\=[tl]*[cfegswpWPBX#bx+]\)' contained +syn match erlangQuotedAtomModifier '\\\%(\o\{1,3}\|x\x\x\|x{\x\+}\|\^.\|.\)' contained +syn match erlangModifier '\$\%([^\\]\|\\\%(\o\{1,3}\|x\x\x\|x{\x\+}\|\^.\|.\)\)' + +" Operators, separators +syn match erlangOperator '==\|=:=\|/=\|=/=\|<\|=<\|>\|>=\|=>\|:=\|++\|--\|=\|!\|<-\|+\|-\|\*\|\/' +syn keyword erlangOperator div rem or xor bor bxor bsl bsr and band not bnot andalso orelse +syn match erlangBracket '{\|}\|\[\|]\||\|||' +syn match erlangPipe '|' +syn match erlangRightArrow '->' + +" Atoms, function calls (order is important) +syn match erlangAtom '\<\l[[:alnum:]_@]*' contains=erlangBoolean +syn keyword erlangBoolean true false contained +syn match erlangLocalFuncCall '\<\a[[:alnum:]_@]*\>\%(\%(\s\|\n\|%.*\n\)*(\)\@=' contains=erlangBIF +syn match erlangLocalFuncRef '\<\a[[:alnum:]_@]*\>\%(\%(\s\|\n\|%.*\n\)*/\)\@=' +syn match erlangGlobalFuncCall '\<\%(\a[[:alnum:]_@]*\%(\s\|\n\|%.*\n\)*\.\%(\s\|\n\|%.*\n\)*\)*\a[[:alnum:]_@]*\%(\s\|\n\|%.*\n\)*:\%(\s\|\n\|%.*\n\)*\a[[:alnum:]_@]*\>\%(\%(\s\|\n\|%.*\n\)*(\)\@=' contains=erlangComment +syn match erlangGlobalFuncRef '\<\%(\a[[:alnum:]_@]*\%(\s\|\n\|%.*\n\)*\.\%(\s\|\n\|%.*\n\)*\)*\a[[:alnum:]_@]*\%(\s\|\n\|%.*\n\)*:\%(\s\|\n\|%.*\n\)*\a[[:alnum:]_@]*\>\%(\%(\s\|\n\|%.*\n\)*/\)\@=' contains=erlangComment + +" Variables, macros, records, maps +syn match erlangVariable '\<[A-Z_][[:alnum:]_@]*' +syn match erlangMacro '??\=[[:alnum:]_@]\+' +syn match erlangMacro '\%(-define(\)\@<=[[:alnum:]_@]\+' +syn match erlangMap '#' +syn match erlangRecord '#\s*\l[[:alnum:]_@]*' + +" Shebang (this line has to be after the ErlangMap) +syn match erlangShebang '^#!.*' + +" Bitstrings +syn match erlangBitType '\%(\/\%(\s\|\n\|%.*\n\)*\)\@<=\%(integer\|float\|binary\|bytes\|bitstring\|bits\|binary\|utf8\|utf16\|utf32\|signed\|unsigned\|big\|little\|native\|unit\)\%(\%(\s\|\n\|%.*\n\)*-\%(\s\|\n\|%.*\n\)*\%(integer\|float\|binary\|bytes\|bitstring\|bits\|binary\|utf8\|utf16\|utf32\|signed\|unsigned\|big\|little\|native\|unit\)\)*' contains=erlangComment + +" Constants and Directives +syn match erlangUnknownAttribute '^\s*-\%(\s\|\n\|%.*\n\)*\l[[:alnum:]_@]*' contains=erlangComment +syn match erlangAttribute '^\s*-\%(\s\|\n\|%.*\n\)*\%(behaviou\=r\|compile\|export\(_type\)\=\|file\|import\|module\|author\|copyright\|doc\|vsn\|on_load\)\>' contains=erlangComment +syn match erlangInclude '^\s*-\%(\s\|\n\|%.*\n\)*\%(include\|include_lib\)\>' contains=erlangComment +syn match erlangRecordDef '^\s*-\%(\s\|\n\|%.*\n\)*record\>' contains=erlangComment +syn match erlangDefine '^\s*-\%(\s\|\n\|%.*\n\)*\%(define\|undef\)\>' contains=erlangComment +syn match erlangPreCondit '^\s*-\%(\s\|\n\|%.*\n\)*\%(ifdef\|ifndef\|else\|endif\)\>' contains=erlangComment +syn match erlangType '^\s*-\%(\s\|\n\|%.*\n\)*\%(spec\|type\|opaque\|callback\)\>' contains=erlangComment + +" Keywords +syn keyword erlangKeyword after begin case catch cond end fun if let of +syn keyword erlangKeyword receive when try + +" Build-in-functions (BIFs) +syn keyword erlangBIF abs alive apply atom_to_binary atom_to_list contained +syn keyword erlangBIF binary_part binary_to_atom contained +syn keyword erlangBIF binary_to_existing_atom binary_to_float contained +syn keyword erlangBIF binary_to_integer bitstring_to_list contained +syn keyword erlangBIF binary_to_list binary_to_term bit_size contained +syn keyword erlangBIF byte_size check_old_code check_process_code contained +syn keyword erlangBIF concat_binary date delete_module demonitor contained +syn keyword erlangBIF disconnect_node element erase error exit contained +syn keyword erlangBIF float float_to_binary float_to_list contained +syn keyword erlangBIF garbage_collect get get_keys group_leader contained +syn keyword erlangBIF halt hd integer_to_binary integer_to_list contained +syn keyword erlangBIF iolist_to_binary iolist_size is_alive contained +syn keyword erlangBIF is_atom is_binary is_bitstring is_boolean contained +syn keyword erlangBIF is_float is_function is_integer is_list contained +syn keyword erlangBIF is_number is_pid is_port is_process_alive contained +syn keyword erlangBIF is_record is_reference is_tuple length link contained +syn keyword erlangBIF list_to_atom list_to_binary contained +syn keyword erlangBIF list_to_bitstring list_to_existing_atom contained +syn keyword erlangBIF list_to_float list_to_integer list_to_pid contained +syn keyword erlangBIF list_to_tuple load_module make_ref max min contained +syn keyword erlangBIF module_loaded monitor monitor_node node contained +syn keyword erlangBIF nodes now open_port pid_to_list port_close contained +syn keyword erlangBIF port_command port_connect pre_loaded contained +syn keyword erlangBIF process_flag process_flag process_info contained +syn keyword erlangBIF process purge_module put register registered contained +syn keyword erlangBIF round self setelement size spawn spawn_link contained +syn keyword erlangBIF spawn_monitor spawn_opt split_binary contained +syn keyword erlangBIF statistics term_to_binary throw time tl contained +syn keyword erlangBIF trunc tuple_size tuple_to_list unlink contained +syn keyword erlangBIF unregister whereis contained + +" Sync at the beginning of functions: if this is not used, multiline string +" are not always recognized, and the indentation script cannot use the +" "searchpair" (because it would not always skip strings and comments when +" looking for keywords and opening parens/brackets). +syn sync match erlangSync grouphere NONE "^[a-z]\s*(" +let b:erlang_syntax_synced = 1 + +" Define the default highlighting. See ":help group-name" for the groups and +" their colors. + +let s:old_style = (exists("g:erlang_old_style_highlight") && + \g:erlang_old_style_highlight == 1) + +" For version 5.7 and earlier: only when not done already +" For version 5.8 and later: only when an item doesn't have highlighting yet +if version >= 508 || !exists("did_erlang_inits") + if version < 508 + let did_erlang_inits = 1 + command -nargs=+ HiLink hi link + else + command -nargs=+ HiLink hi def link + endif + + " Comments + HiLink erlangComment Comment + HiLink erlangCommentAnnotation Special + HiLink erlangTodo Todo + HiLink erlangShebang Comment + + " Numbers + HiLink erlangNumberInteger Number + HiLink erlangNumberFloat Float + + " Strings, atoms, characters + HiLink erlangString String + + if s:old_style + HiLink erlangQuotedAtom Type + else + HiLink erlangQuotedAtom String + endif + + HiLink erlangStringModifier Special + HiLink erlangQuotedAtomModifier Special + HiLink erlangModifier Special + + " Operators, separators + HiLink erlangOperator Operator + HiLink erlangRightArrow Operator + if s:old_style + HiLink erlangBracket Normal + HiLink erlangPipe Normal + else + HiLink erlangBracket Delimiter + HiLink erlangPipe Delimiter + endif + + " Atoms, functions, variables, macros + if s:old_style + HiLink erlangAtom Normal + HiLink erlangLocalFuncCall Normal + HiLink erlangLocalFuncRef Normal + HiLink erlangGlobalFuncCall Function + HiLink erlangGlobalFuncRef Function + HiLink erlangVariable Normal + HiLink erlangMacro Normal + HiLink erlangRecord Normal + HiLink erlangMap Normal + else + HiLink erlangAtom String + HiLink erlangLocalFuncCall Normal + HiLink erlangLocalFuncRef Normal + HiLink erlangGlobalFuncCall Normal + HiLink erlangGlobalFuncRef Normal + HiLink erlangVariable Identifier + HiLink erlangMacro Macro + HiLink erlangRecord Structure + HiLink erlangMap Structure + endif + + " Bitstrings + if !s:old_style + HiLink erlangBitType Type + endif + + " Constants and Directives + if s:old_style + HiLink erlangAttribute Type + HiLink erlangMacroDef Type + HiLink erlangUnknownAttribute Normal + HiLink erlangInclude Type + HiLink erlangRecordDef Type + HiLink erlangDefine Type + HiLink erlangPreCondit Type + HiLink erlangType Type + else + HiLink erlangAttribute Keyword + HiLink erlangMacroDef Macro + HiLink erlangUnknownAttribute Normal + HiLink erlangInclude Include + HiLink erlangRecordDef Keyword + HiLink erlangDefine Define + HiLink erlangPreCondit PreCondit + HiLink erlangType Type + endif + + " Keywords + HiLink erlangKeyword Keyword + + " Build-in-functions (BIFs) + HiLink erlangBIF Function + + if s:old_style + HiLink erlangBoolean Statement + HiLink erlangExtra Statement + HiLink erlangSignal Statement + else + HiLink erlangBoolean Boolean + HiLink erlangExtra Statement + HiLink erlangSignal Statement + endif + + delcommand HiLink endif -" }}} let b:current_syntax = "erlang" -" vim: set foldmethod=marker: +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 et