summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--autoload/crystal_lang.vim332
-rw-r--r--autoload/vital.vim16
-rw-r--r--autoload/vital/_crystal.vim313
-rw-r--r--autoload/vital/_crystal/ColorEcho.vim182
-rw-r--r--autoload/vital/_crystal/Data/List.vim446
-rw-r--r--autoload/vital/_crystal/Data/String.vim572
-rw-r--r--autoload/vital/_crystal/Prelude.vim389
-rw-r--r--autoload/vital/_crystal/Process.vim185
-rw-r--r--autoload/vital/_crystal/Web/JSON.vim112
-rwxr-xr-xbuild1
-rw-r--r--ftdetect/polyglot.vim6
-rw-r--r--ftplugin/crystal.vim60
-rw-r--r--indent/crystal.vim639
-rw-r--r--syntax/crystal.vim393
15 files changed, 3647 insertions, 0 deletions
diff --git a/README.md b/README.md
index 7203d13e..a56e728c 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ Optionally download one of the [releases](https://github.com/sheerun/vim-polyglo
- [clojure](https://github.com/guns/vim-clojure-static) (syntax, indent, autoload, ftplugin, ftdetect)
- [coffee-script](https://github.com/kchmck/vim-coffee-script) (syntax, indent, compiler, autoload, ftplugin, ftdetect)
- [cryptol](https://github.com/victoredwardocallaghan/cryptol.vim) (syntax, compiler, ftplugin, ftdetect)
+- [crystal](https://github.com/rhysd/vim-crystal) (syntax, indent, autoload, ftplugin, ftdetect)
- [cql](https://github.com/elubow/cql-vim) (syntax, ftdetect)
- [css](https://github.com/JulesWang/css.vim) (syntax)
- [cucumber](https://github.com/tpope/vim-cucumber) (syntax, indent, compiler, ftplugin, ftdetect)
diff --git a/autoload/crystal_lang.vim b/autoload/crystal_lang.vim
new file mode 100644
index 00000000..8b1252d2
--- /dev/null
+++ b/autoload/crystal_lang.vim
@@ -0,0 +1,332 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+let s:V = vital#of('crystal')
+let s:P = s:V.import('Process')
+let s:J = s:V.import('Web.JSON')
+let s:C = s:V.import('ColorEcho')
+
+function! s:echo_error(msg, ...) abort
+ echohl ErrorMsg
+ if a:0 == 0
+ echomsg a:msg
+ else
+ echomsg call('printf', [a:msg] + a:000)
+ endif
+ echohl None
+endfunction
+
+function! s:run_cmd(cmd) abort
+ if !executable(g:crystal_compiler_command)
+ throw "vim-crystal: Error: '" . g:crystal_compiler_command . "' command is not found."
+ endif
+ return s:P.system(a:cmd)
+endfunction
+
+function! s:find_root_by_spec(d) abort
+ let dir = finddir('spec', a:d . ';')
+ if dir ==# ''
+ return ''
+ endif
+
+ " Note: ':h:h' for {root}/spec/ -> {root}/spec -> {root}
+ return fnamemodify(dir, ':p:h:h')
+endfunction
+
+function! crystal_lang#entrypoint_for(file_path) abort
+ let parent_dir = fnamemodify(a:file_path, ':p:h')
+ let root_dir = s:find_root_by_spec(parent_dir)
+ if root_dir ==# ''
+ " No spec diretory found. No need to make temporary file
+ return a:file_path
+ endif
+
+ let temp_name = root_dir . '/__vim-crystal-temporary-entrypoint-' . fnamemodify(a:file_path, ':t')
+ let contents = [
+ \ 'require "spec"',
+ \ 'require "./spec/**"',
+ \ printf('require "./%s"', fnamemodify(a:file_path, ':p')[strlen(root_dir)+1 : ])
+ \ ]
+
+ let result = writefile(contents, temp_name)
+ if result == -1
+ " Note: When writefile() failed
+ return a:file_path
+ endif
+
+ return temp_name
+endfunction
+
+function! crystal_lang#tool(name, file, pos, option_str) abort
+ let entrypoint = crystal_lang#entrypoint_for(a:file)
+ let cmd = printf(
+ \ '%s tool %s --no-color %s --cursor %s:%d:%d %s',
+ \ g:crystal_compiler_command,
+ \ a:name,
+ \ a:option_str,
+ \ a:file,
+ \ a:pos[1],
+ \ a:pos[2],
+ \ entrypoint
+ \ )
+
+ try
+ let output = s:run_cmd(cmd)
+ return {'failed': s:P.get_last_status(), 'output': output}
+ finally
+ " Note:
+ " If the entry point is temporary file, delete it finally.
+ if a:file !=# entrypoint
+ call delete(entrypoint)
+ endif
+ endtry
+endfunction
+
+" `pos` is assumed a returned value from getpos()
+function! crystal_lang#impl(file, pos, option_str) abort
+ return crystal_lang#tool('implementations', a:file, a:pos, a:option_str)
+endfunction
+
+function! s:jump_to_impl(impl) abort
+ execute 'edit' a:impl.filename
+ call cursor(a:impl.line, a:impl.column)
+endfunction
+
+function! crystal_lang#jump_to_definition(file, pos) abort
+ echo 'analyzing definitions under cursor...'
+
+ let cmd_result = crystal_lang#impl(a:file, a:pos, '--format json')
+ if cmd_result.failed
+ return s:echo_error(cmd_result.output)
+ endif
+
+ let impl = s:J.decode(cmd_result.output)
+ if impl.status !=# 'ok'
+ return s:echo_error(impl.message)
+ endif
+
+ if len(impl.implementations) == 1
+ call s:jump_to_impl(impl.implementations[0])
+ return
+ endif
+
+ let message = "Multiple definitions detected. Choose a number\n\n"
+ for idx in range(len(impl.implementations))
+ let i = impl.implementations[idx]
+ let message .= printf("[%d] %s:%d:%d\n", idx, i.filename, i.line, i.column)
+ endfor
+ let message .= "\n"
+ let idx = str2nr(input(message, "\n> "))
+ call s:jump_to_impl(impl.implementations[idx])
+endfunction
+
+function! crystal_lang#context(file, pos, option_str) abort
+ return crystal_lang#tool('context', a:file, a:pos, a:option_str)
+endfunction
+
+function! crystal_lang#type_hierarchy(file, option_str) abort
+ let cmd = printf(
+ \ '%s tool hierarchy --no-color %s %s',
+ \ g:crystal_compiler_command,
+ \ a:option_str,
+ \ a:file
+ \ )
+
+ return s:run_cmd(cmd)
+endfunction
+
+function! s:find_completion_start() abort
+ let c = col('.')
+ if c <= 1
+ return -1
+ endif
+
+ let line = getline('.')[:c-2]
+ return match(line, '\w\+$')
+endfunction
+
+function! crystal_lang#complete(findstart, base) abort
+ if a:findstart
+ echom 'find start'
+ return s:find_completion_start()
+ endif
+
+ let cmd_result = crystal_lang#context(expand('%'), getpos('.'), '--format json')
+ if cmd_result.failed
+ return
+ endif
+
+ let contexts = s:J.decode(cmd_result.output)
+ if contexts.status !=# 'ok'
+ return
+ endif
+
+ let candidates = []
+
+ for c in contexts.contexts
+ for [name, desc] in items(c)
+ let candidates += [{
+ \ 'word': name,
+ \ 'menu': ': ' . desc . ' [var]',
+ \ }]
+ endfor
+ endfor
+
+ return candidates
+endfunction
+
+function! crystal_lang#get_spec_switched_path(absolute_path) abort
+ let base = fnamemodify(a:absolute_path, ':t:r')
+
+ " TODO: Make cleverer
+ if base =~# '_spec$'
+ let parent = fnamemodify(substitute(a:absolute_path, '/spec/', '/src/', ''), ':h')
+ return parent . '/' . matchstr(base, '.\+\ze_spec$') . '.cr'
+ else
+ let parent = fnamemodify(substitute(a:absolute_path, '/src/', '/spec/', ''), ':h')
+ return parent . '/' . base . '_spec.cr'
+ endif
+endfunction
+
+function! crystal_lang#switch_spec_file(...) abort
+ let path = a:0 == 0 ? expand('%:p') : fnamemodify(a:1, ':p')
+ if path !~# '.cr$'
+ return s:echo_error('Not crystal source file: ' . path)
+ endif
+
+ execute 'edit!' crystal_lang#get_spec_switched_path(path)
+endfunction
+
+function! s:run_spec(root, path, ...) abort
+ " Note:
+ " `crystal spec` can't understand absolute path.
+ let cmd = printf(
+ \ '%s spec %s%s',
+ \ g:crystal_compiler_command,
+ \ a:path,
+ \ a:0 == 0 ? '' : (':' . a:1)
+ \ )
+
+ let saved_cwd = getcwd()
+ let cd = haslocaldir() ? 'lcd' : 'cd'
+ try
+ execute cd a:root
+ call s:C.echo(s:run_cmd(cmd))
+ finally
+ execute cd saved_cwd
+ endtry
+endfunction
+
+function! crystal_lang#run_all_spec(...) abort
+ let path = a:0 == 0 ? expand('%:p:h') : a:1
+ let root_path = s:find_root_by_spec(path)
+ if root_path ==# ''
+ return s:echo_error("'spec' directory is not found")
+ endif
+ call s:run_spec(root_path, 'spec')
+endfunction
+
+function! crystal_lang#run_current_spec(...) abort
+ " /foo/bar/src/poyo.cr
+ let path = a:0 == 0 ? expand('%:p') : fnamemodify(a:1, ':p')
+ if path !~# '.cr$'
+ return s:echo_error('Not crystal source file: ' . path)
+ endif
+
+ " /foo/bar/src
+ let source_dir = fnamemodify(path, ':h')
+
+ " /foo/bar
+ let root_dir = s:find_root_by_spec(source_dir)
+ if root_dir ==# ''
+ return s:echo_error("'spec' directory is not found")
+ endif
+
+ " src
+ let rel_path = source_dir[strlen(root_dir)+1 : ]
+
+ if path =~# '_spec.cr$'
+ call s:run_spec(root_dir, path[strlen(root_dir)+1 : ], line('.'))
+ else
+ let spec_path = substitute(rel_path, '^src', 'spec', '') . '/' . fnamemodify(path, ':t:r') . '_spec.cr'
+ if !filereadable(root_dir . '/' . spec_path)
+ return s:echo_error('Error: Could not find a spec source corresponding to ' . path)
+ endif
+ call s:run_spec(root_dir, spec_path)
+ endif
+endfunction
+
+function! crystal_lang#format_string(code, ...) abort
+ let cmd = printf(
+ \ '%s tool format --no-color %s -',
+ \ g:crystal_compiler_command,
+ \ get(a:, 1, '')
+ \ )
+ let output = s:P.system(cmd, a:code)
+ if s:P.get_last_status()
+ throw 'vim-crystal: Error on formatting: ' . output
+ endif
+ return output
+endfunction
+
+function! s:get_saved_states() abort
+ let result = {}
+ let fname = bufname('%')
+ let current_winnr = winnr()
+ for i in range(1, winnr('$'))
+ let bufnr = winbufnr(i)
+ if bufnr == -1
+ continue
+ endif
+ if bufname(bufnr) ==# fname
+ execute i 'wincmd w'
+ let result[i] = {
+ \ 'pos': getpos('.'),
+ \ 'screen': winsaveview()
+ \ }
+ endif
+ endfor
+ execute current_winnr 'wincmd w'
+ return result
+endfunction
+
+function! crystal_lang#format(option_str) abort
+ if !executable(g:crystal_compiler_command)
+ " Finish command silently
+ return
+ endif
+
+ let formatted = crystal_lang#format_string(join(getline(1, '$'), "\n"), a:option_str)
+ let formatted = substitute(formatted, '\n$', '', '')
+
+ let sel_save = &l:selection
+ let ve_save = &virtualedit
+ let &l:selection = 'inclusive'
+ let &virtualedit = ''
+ let [save_g_reg, save_g_regtype] = [getreg('g'), getregtype('g')]
+ let windows_save = s:get_saved_states()
+
+ try
+ call setreg('g', formatted, 'v')
+ silent normal! ggvG$"gp
+ finally
+ call setreg('g', save_g_reg, save_g_regtype)
+ let &l:selection = sel_save
+ let &virtualedit = ve_save
+ let winnr = winnr()
+ for winnr in keys(windows_save)
+ let w = windows_save[winnr]
+ execute winnr 'wincmd w'
+ call setpos('.', w.pos)
+ call winrestview(w.screen)
+ endfor
+ execute winnr 'wincmd w'
+ endtry
+endfunction
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+endif
diff --git a/autoload/vital.vim b/autoload/vital.vim
new file mode 100644
index 00000000..98fe948f
--- /dev/null
+++ b/autoload/vital.vim
@@ -0,0 +1,16 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+function! vital#of(name) abort
+ let files = globpath(&runtimepath, 'autoload/vital/' . a:name . '.vital')
+ let file = split(files, "\n")
+ if empty(file)
+ throw 'vital: version file not found: ' . a:name
+ endif
+ let ver = readfile(file[0], 'b')
+ if empty(ver)
+ throw 'vital: invalid version file: ' . a:name
+ endif
+ return vital#_{substitute(ver[0], '\W', '', 'g')}#new()
+endfunction
+
+endif
diff --git a/autoload/vital/_crystal.vim b/autoload/vital/_crystal.vim
new file mode 100644
index 00000000..f5ff37f3
--- /dev/null
+++ b/autoload/vital/_crystal.vim
@@ -0,0 +1,313 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+let s:self_version = expand('<sfile>:t:r')
+let s:self_file = expand('<sfile>')
+
+" Note: The extra argument to globpath() was added in Patch 7.2.051.
+let s:globpath_third_arg = v:version > 702 || v:version == 702 && has('patch51')
+
+let s:loaded = {}
+let s:cache_module_path = {}
+let s:cache_sid = {}
+
+let s:_vital_files_cache_runtimepath = ''
+let s:_vital_files_cache = []
+let s:_unify_path_cache = {}
+
+function! s:import(name, ...) abort
+ let target = {}
+ let functions = []
+ for a in a:000
+ if type(a) == type({})
+ let target = a
+ elseif type(a) == type([])
+ let functions = a
+ endif
+ unlet a
+ endfor
+ let module = s:_import(a:name)
+ if empty(functions)
+ call extend(target, module, 'keep')
+ else
+ for f in functions
+ if has_key(module, f) && !has_key(target, f)
+ let target[f] = module[f]
+ endif
+ endfor
+ endif
+ return target
+endfunction
+
+function! s:load(...) dict abort
+ for arg in a:000
+ let [name; as] = type(arg) == type([]) ? arg[: 1] : [arg, arg]
+ let target = split(join(as, ''), '\W\+')
+ let dict = self
+ let dict_type = type({})
+ while !empty(target)
+ let ns = remove(target, 0)
+ if !has_key(dict, ns)
+ let dict[ns] = {}
+ endif
+ if type(dict[ns]) == dict_type
+ let dict = dict[ns]
+ else
+ unlet dict
+ break
+ endif
+ endwhile
+
+ if exists('dict')
+ call extend(dict, s:_import(name))
+ endif
+ unlet arg
+ endfor
+ return self
+endfunction
+
+function! s:unload() abort
+ let s:loaded = {}
+ let s:cache_sid = {}
+ let s:cache_module_path = {}
+endfunction
+
+function! s:exists(name) abort
+ return s:_get_module_path(a:name) !=# ''
+endfunction
+
+function! s:search(pattern) abort
+ let paths = s:_vital_files(a:pattern)
+ let modules = sort(map(paths, 's:_file2module(v:val)'))
+ return s:_uniq(modules)
+endfunction
+
+function! s:expand_modules(entry, all) abort
+ if type(a:entry) == type([])
+ let candidates = s:_concat(map(copy(a:entry), 's:search(v:val)'))
+ if empty(candidates)
+ throw printf('vital: Any of module %s is not found', string(a:entry))
+ endif
+ if eval(join(map(copy(candidates), 'has_key(a:all, v:val)'), '+'))
+ let modules = []
+ else
+ let modules = [candidates[0]]
+ endif
+ else
+ let modules = s:search(a:entry)
+ if empty(modules)
+ throw printf('vital: Module %s is not found', a:entry)
+ endif
+ endif
+ call filter(modules, '!has_key(a:all, v:val)')
+ for module in modules
+ let a:all[module] = 1
+ endfor
+ return modules
+endfunction
+
+function! s:_import(name) abort
+ if type(a:name) == type(0)
+ return s:_build_module(a:name)
+ endif
+ let path = s:_get_module_path(a:name)
+ if path ==# ''
+ throw 'vital: module not found: ' . a:name
+ endif
+ let sid = s:_get_sid_by_script(path)
+ if !sid
+ try
+ execute 'source' fnameescape(path)
+ catch /^Vim\%((\a\+)\)\?:E484/
+ throw 'vital: module not found: ' . a:name
+ catch /^Vim\%((\a\+)\)\?:E127/
+ " Ignore.
+ endtry
+
+ let sid = s:_get_sid_by_script(path)
+ endif
+ return s:_build_module(sid)
+endfunction
+
+function! s:_get_module_path(name) abort
+ let key = a:name . '_'
+ if has_key(s:cache_module_path, key)
+ return s:cache_module_path[key]
+ endif
+ if s:_is_absolute_path(a:name) && filereadable(a:name)
+ return a:name
+ endif
+ if a:name ==# ''
+ let paths = [s:self_file]
+ elseif a:name =~# '\v^\u\w*%(\.\u\w*)*$'
+ let paths = s:_vital_files(a:name)
+ else
+ throw 'vital: Invalid module name: ' . a:name
+ endif
+
+ call filter(paths, 'filereadable(expand(v:val, 1))')
+ let path = get(paths, 0, '')
+ let s:cache_module_path[key] = path
+ return path
+endfunction
+
+function! s:_get_sid_by_script(path) abort
+ if has_key(s:cache_sid, a:path)
+ return s:cache_sid[a:path]
+ endif
+
+ let path = s:_unify_path(a:path)
+ for line in filter(split(s:_redir('scriptnames'), "\n"),
+ \ 'stridx(v:val, s:self_version) > 0')
+ let list = matchlist(line, '^\s*\(\d\+\):\s\+\(.\+\)\s*$')
+ if !empty(list) && s:_unify_path(list[2]) ==# path
+ let s:cache_sid[a:path] = list[1] - 0
+ return s:cache_sid[a:path]
+ endif
+ endfor
+ return 0
+endfunction
+
+function! s:_file2module(file) abort
+ let filename = fnamemodify(a:file, ':p:gs?[\\/]?/?')
+ let tail = matchstr(filename, 'autoload/vital/_\w\+/\zs.*\ze\.vim$')
+ return join(split(tail, '[\\/]\+'), '.')
+endfunction
+
+if filereadable(expand('<sfile>:r') . '.VIM')
+ " resolve() is slow, so we cache results.
+ " Note: On windows, vim can't expand path names from 8.3 formats.
+ " So if getting full path via <sfile> and $HOME was set as 8.3 format,
+ " vital load duplicated scripts. Below's :~ avoid this issue.
+ function! s:_unify_path(path) abort
+ if has_key(s:_unify_path_cache, a:path)
+ return s:_unify_path_cache[a:path]
+ endif
+ let value = tolower(fnamemodify(resolve(fnamemodify(
+ \ a:path, ':p')), ':~:gs?[\\/]?/?'))
+ let s:_unify_path_cache[a:path] = value
+ return value
+ endfunction
+else
+ function! s:_unify_path(path) abort
+ return resolve(fnamemodify(a:path, ':p:gs?[\\/]?/?'))
+ endfunction
+endif
+
+if s:globpath_third_arg
+ function! s:_runtime_files(path) abort
+ return split(globpath(&runtimepath, a:path, 1), "\n")
+ endfunction
+else
+ function! s:_runtime_files(path) abort
+ return split(globpath(&runtimepath, a:path), "\n")
+ endfunction
+endif
+
+function! s:_vital_files(pattern) abort
+ if s:_vital_files_cache_runtimepath !=# &runtimepath
+ let path = printf('autoload/vital/%s/**/*.vim', s:self_version)
+ let s:_vital_files_cache = s:_runtime_files(path)
+ let mod = ':p:gs?[\\/]\+?/?'
+ call map(s:_vital_files_cache, 'fnamemodify(v:val, mod)')
+ let s:_vital_files_cache_runtimepath = &runtimepath
+ endif
+ let target = substitute(a:pattern, '\.', '/', 'g')
+ let target = substitute(target, '\*', '[^/]*', 'g')
+ let regexp = printf('autoload/vital/%s/%s.vim', s:self_version, target)
+ return filter(copy(s:_vital_files_cache), 'v:val =~# regexp')
+endfunction
+
+" Copy from System.Filepath
+if has('win16') || has('win32') || has('win64')
+ function! s:_is_absolute_path(path) abort
+ return a:path =~? '^[a-z]:[/\\]'
+ endfunction
+else
+ function! s:_is_absolute_path(path) abort
+ return a:path[0] ==# '/'
+ endfunction
+endif
+
+function! s:_build_module(sid) abort
+ if has_key(s:loaded, a:sid)
+ return copy(s:loaded[a:sid])
+ endif
+ let functions = s:_get_functions(a:sid)
+
+ let prefix = '<SNR>' . a:sid . '_'
+ let module = {}
+ for func in functions
+ let module[func] = function(prefix . func)
+ endfor
+ if has_key(module, '_vital_created')
+ call module._vital_created(module)
+ endif
+ let export_module = filter(copy(module), 'v:key =~# "^\\a"')
+ let s:loaded[a:sid] = get(g:, 'vital_debug', 0) ? module : export_module
+ if has_key(module, '_vital_loaded')
+ let V = vital#{s:self_version}#new()
+ call module._vital_loaded(V)
+ endif
+ return copy(s:loaded[a:sid])
+endfunction
+
+if exists('+regexpengine')
+ function! s:_get_functions(sid) abort
+ let funcs = s:_redir(printf("function /\\%%#=2^\<SNR>%d_", a:sid))
+ let map_pat = '<SNR>' . a:sid . '_\zs\w\+'
+ return map(split(funcs, "\n"), 'matchstr(v:val, map_pat)')
+ endfunction
+else
+ function! s:_get_functions(sid) abort
+ let prefix = '<SNR>' . a:sid . '_'
+ let funcs = s:_redir('function')
+ let filter_pat = '^\s*function ' . prefix
+ let map_pat = prefix . '\zs\w\+'
+ return map(filter(split(funcs, "\n"),
+ \ 'stridx(v:val, prefix) > 0 && v:val =~# filter_pat'),
+ \ 'matchstr(v:val, map_pat)')
+ endfunction
+endif
+
+if exists('*uniq')
+ function! s:_uniq(list) abort
+ return uniq(a:list)
+ endfunction
+else
+ function! s:_uniq(list) abort
+ let i = len(a:list) - 1
+ while 0 < i
+ if a:list[i] ==# a:list[i - 1]
+ call remove(a:list, i)
+ let i -= 2
+ else
+ let i -= 1
+ endif
+ endwhile
+ return a:list
+ endfunction
+endif
+
+function! s:_concat(lists) abort
+ let result_list = []
+ for list in a:lists
+ let result_list += list
+ endfor
+ return result_list
+endfunction
+
+function! s:_redir(cmd) abort
+ let [save_verbose, save_verbosefile] = [&verbose, &verbosefile]
+ set verbose=0 verbosefile=
+ redir => res
+ silent! execute a:cmd
+ redir END
+ let [&verbose, &verbosefile] = [save_verbose, save_verbosefile]
+ return res
+endfunction
+
+function! vital#{s:self_version}#new() abort
+ return s:_import('')
+endfunction
+
+endif
diff --git a/autoload/vital/_crystal/ColorEcho.vim b/autoload/vital/_crystal/ColorEcho.vim
new file mode 100644
index 00000000..f2eae658
--- /dev/null
+++ b/autoload/vital/_crystal/ColorEcho.vim
@@ -0,0 +1,182 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+scriptencoding utf-8
+let s:save_cpo = &cpo
+set cpo&vim
+
+function! s:_is_available() abort
+ if has('gui_running')
+ return 1
+ endif
+
+ if has('win32') || has('win64')
+ return 0
+ endif
+
+ return exists('&t_Co') && (&t_Co == 8 || &t_Co == 256)
+endfunction
+
+function! s:is_available() abort
+ if exists('s:is_available_cache')
+ return s:is_available_cache
+ endif
+ let s:is_available_cache = s:_is_available()
+ return s:is_available_cache
+endfunction
+
+function! s:_define_ansi_highlights() abort
+ hi ansiNone cterm=NONE gui=NONE
+
+ hi ansiBlackBg ctermbg=black guibg=black cterm=none gui=none
+ hi ansiRedBg ctermbg=red guibg=red cterm=none gui=none
+ hi ansiGreenBg ctermbg=green guibg=green cterm=none gui=none
+ hi ansiYellowBg ctermbg=yellow guibg=yellow cterm=none gui=none
+ hi ansiBlueBg ctermbg=blue guibg=blue cterm=none gui=none
+ hi ansiMagentaBg ctermbg=magenta guibg=magenta cterm=none gui=none
+ hi ansiCyanBg ctermbg=cyan guibg=cyan cterm=none gui=none
+ hi ansiWhiteBg ctermbg=white guibg=white cterm=none gui=none
+ hi ansiGrayBg ctermbg=gray guibg=gray cterm=none gui=none
+
+ hi ansiBlackFg ctermfg=black guifg=black cterm=none gui=none
+ hi ansiRedFg ctermfg=red guifg=red cterm=none gui=none
+ hi ansiGreenFg ctermfg=green guifg=green cterm=none gui=none
+ hi ansiYellowFg ctermfg=yellow guifg=yellow cterm=none gui=none
+ hi ansiBlueFg ctermfg=blue guifg=blue cterm=none gui=none
+ hi ansiMagentaFg ctermfg=magenta guifg=magenta cterm=none gui=none
+ hi ansiCyanFg ctermfg=cyan guifg=cyan cterm=none gui=none
+ hi ansiWhiteFg ctermfg=white guifg=white cterm=none gui=none
+ hi ansiGrayFg ctermfg=gray guifg=gray cterm=none gui=none
+
+ hi ansiBoldBlackFg ctermfg=black guifg=black cterm=none gui=none cterm=bold gui=bold
+ hi ansiBoldRedFg ctermfg=red guifg=red cterm=none gui=none cterm=bold gui=bold
+ hi ansiBoldGreenFg ctermfg=green guifg=green cterm=none gui=none cterm=bold gui=bold
+ hi ansiBoldYellowFg ctermfg=yellow guifg=yellow cterm=none gui=none cterm=bold gui=bold
+ hi ansiBoldBlueFg ctermfg=blue guifg=blue cterm=none gui=none cterm=bold gui=bold
+ hi ansiBoldMagentaFg ctermfg=magenta guifg=magenta cterm=none gui=none cterm=bold gui=bold
+ hi ansiBoldCyanFg ctermfg=cyan guifg=cyan cterm=none gui=none cterm=bold gui=bold
+ hi ansiBoldWhiteFg ctermfg=white guifg=white cterm=none gui=none cterm=bold gui=bold
+ hi ansiBoldGrayFg ctermfg=gray guifg=gray cterm=none gui=none cterm=bold gui=bold
+
+ hi ansiUnderlineBlackFg ctermfg=black guifg=black cterm=none gui=none cterm=underline gui=underline
+ hi ansiUnderlineRedFg ctermfg=red guifg=red cterm=none gui=none cterm=underline gui=underline
+ hi ansiUnderlineGreenFg ctermfg=green guifg=green cterm=none gui=none cterm=underline gui=underline
+ hi ansiUnderlineYellowFg ctermfg=yellow guifg=yellow cterm=none gui=none cterm=underline gui=underline
+ hi ansiUnderlineBlueFg ctermfg=blue guifg=blue cterm=none gui=none cterm=underline gui=underline
+ hi ansiUnderlineMagentaFg ctermfg=magenta guifg=magenta cterm=none gui=none cterm=underline gui=underline
+ hi ansiUnderlineCyanFg ctermfg=cyan guifg=cyan cterm=none gui=none cterm=underline gui=underline
+ hi ansiUnderlineWhiteFg ctermfg=white guifg=white cterm=none gui=none cterm=underline gui=underline
+ hi ansiUnderlineGrayFg ctermfg=gray guifg=gray cterm=none gui=none cterm=underline gui=underline
+
+endfunction
+
+let s:echorizer = {
+ \ 'value': '',
+ \ 'attr': '',
+ \ }
+
+function s:echorizer.eat() abort
+ let matched = match(self.value, '\e\[\d*;\=m')
+ if matched == -1
+ return {}
+ endif
+
+ let matched_end = matchend(self.value, '\e\[\d*;\=m')
+
+ let token = {
+ \ 'body': matched == 0 ? '' : self.value[ : matched-1],
+ \ 'code': matchstr(self.value[matched : matched_end-1], '\d\+')
+ \ }
+
+ let self.value = self.value[matched_end : ]
+
+ return token
+endfunction
+
+let s:COLORS = {
+ \ 0: "None",
+ \ 30: "BlackFg",
+ \ 31: "RedFg",
+ \ 32: "GreenFg",
+ \ 33: "YellowFg",
+ \ 34: "BlueFg",
+ \ 35: "MagentaFg",
+ \ 36: "CyanFg",
+ \ 37: "WhiteFg",
+ \ 40: "BlackBg",
+ \ 41: "RedBg",
+ \ 42: "GreenBg",
+ \ 43: "YellowBg",
+ \ 44: "BlueBg",
+ \ 45: "MagentaBg",
+ \ 46: "CyanBg",
+ \ 47: "WhiteBg",
+ \ 90: "GrayFg",
+ \ }
+
+function s:echorizer.echo_ansi(code) abort
+ if !has_key(s:COLORS, a:code)
+ return
+ endif
+
+ execute 'echohl' 'ansi' . self.attr . s:COLORS[a:code]
+
+ if a:code == 0
+ let self.attr = ''
+ endif
+endfunction
+
+function s:echorizer.echo() abort
+ echo
+
+ while 1
+ let token = self.eat()
+ if token == {}
+ break
+ endif
+
+ if token.body !=# ''
+ echon token.body
+ endif
+
+ " TODO: Now only one attribute can be specified
+ if token.code == 1
+ let self.attr = 'Bold'
+ elseif token.code == 4
+ let self.attr = 'Underline'
+ elseif token.code ==# ''
+ call self.echo_ansi(0)
+ else
+ call self.echo_ansi(token.code)
+ endif
+ endwhile
+
+ echon self.value
+ echohl None
+ let self.value = ''
+endfunction
+
+function! s:get_echorizer(str) abort
+ let e = deepcopy(s:echorizer)
+ let e.value = a:str
+ return e
+endfunction
+
+function! s:echo(str) abort
+ if !s:is_available()
+ echo substitute(a:str, '\e[.*m', '', 'g')
+ return
+ endif
+
+ if !exists('g:__vital_color_echo_already_highlighted')
+ call s:_define_ansi_highlights()
+ let g:__vital_color_echo_already_highlighted = 1
+ endif
+
+ let echorizer = s:get_echorizer(a:str)
+ call echorizer.echo()
+endfunction
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+endif
diff --git a/autoload/vital/_crystal/Data/List.vim b/autoload/vital/_crystal/Data/List.vim
new file mode 100644
index 00000000..55703b3d
--- /dev/null
+++ b/autoload/vital/_crystal/Data/List.vim
@@ -0,0 +1,446 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+" Utilities for list.
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+function! s:pop(list) abort
+ return remove(a:list, -1)
+endfunction
+
+function! s:push(list, val) abort
+ call add(a:list, a:val)
+ return a:list
+endfunction
+
+function! s:shift(list) abort
+ return remove(a:list, 0)
+endfunction
+
+function! s:unshift(list, val) abort
+ return insert(a:list, a:val)
+endfunction
+
+function! s:cons(x, xs) abort
+ return [a:x] + a:xs
+endfunction
+
+function! s:conj(xs, x) abort
+ return a:xs + [a:x]
+endfunction
+
+" Removes duplicates from a list.
+function! s:uniq(list) abort
+ return s:uniq_by(a:list, 'v:val')
+endfunction
+
+" Removes duplicates from a list.
+function! s:uniq_by(list, f) abort
+ let list = map(copy(a:list), printf('[v:val, %s]', a:f))
+ let i = 0
+ let seen = {}
+ while i < len(list)
+ let key = string(list[i][1])
+ if has_key(seen, key)
+ call remove(list, i)
+ else
+ let seen[key] = 1
+ let i += 1
+ endif
+ endwhile
+ return map(list, 'v:val[0]')
+endfunction
+
+function! s:clear(list) abort
+ if !empty(a:list)
+ unlet! a:list[0 : len(a:list) - 1]
+ endif
+ return a:list
+endfunction
+
+" Concatenates a list of lists.
+" XXX: Should we verify the input?
+function! s:concat(list) abort
+ let memo = []
+ for Value in a:list
+ let memo += Value
+ endfor
+ return memo
+endfunction
+
+" Take each elements from lists to a new list.
+function! s:flatten(list, ...) abort
+ let limit = a:0 > 0 ? a:1 : -1
+ let memo = []
+ if limit == 0
+ return a:list
+ endif
+ let limit -= 1
+ for Value in a:list
+ let memo +=
+ \ type(Value) == type([]) ?
+ \ s:flatten(Value, limit) :
+ \ [Value]
+ unlet! Value
+ endfor
+ return memo
+endfunction
+
+" Sorts a list with expression to compare each two values.
+" a:a and a:b can be used in {expr}.
+function! s:sort(list, expr) abort
+ if type(a:expr) == type(function('function'))
+ return sort(a:list, a:expr)
+ endif
+ let s:expr = a:expr
+ return sort(a:list, 's:_compare')
+endfunction
+
+function! s:_compare(a, b) abort
+ return eval(s:expr)
+endfunction
+
+" Sorts a list using a set of keys generated by mapping the values in the list
+" through the given expr.
+" v:val is used in {expr}
+function! s:sort_by(list, expr) abort
+ let pairs = map(a:list, printf('[v:val, %s]', a:expr))
+ return map(s:sort(pairs,
+ \ 'a:a[1] ==# a:b[1] ? 0 : a:a[1] ># a:b[1] ? 1 : -1'), 'v:val[0]')
+endfunction
+
+" Returns a maximum value in {list} through given {expr}.
+" Returns 0 if {list} is empty.
+" v:val is used in {expr}
+function! s:max_by(list, expr) abort
+ if empty(a:list)
+ return 0
+ endif
+ let list = map(copy(a:list), a:expr)
+ return a:list[index(list, max(list))]
+endfunction
+
+" Returns a minimum value in {list} through given {expr}.
+" Returns 0 if {list} is empty.
+" v:val is used in {expr}
+" FIXME: -0x80000000 == 0x80000000
+function! s:min_by(list, expr) abort
+ return s:max_by(a:list, '-(' . a:expr . ')')
+endfunction
+
+" Returns List of character sequence between [a:from, a:to]
+" e.g.: s:char_range('a', 'c') returns ['a', 'b', 'c']
+function! s:char_range(from, to) abort
+ return map(
+ \ range(char2nr(a:from), char2nr(a:to)),
+ \ 'nr2char(v:val)'
+ \)
+endfunction
+
+" Returns true if a:list has a:value.
+" Returns false otherwise.
+function! s:has(list, value) abort
+ return index(a:list, a:value) isnot -1
+endfunction
+
+" Returns true if a:list[a:index] exists.
+" Returns false otherwise.
+" NOTE: Returns false when a:index is negative number.
+function! s:has_index(list, index) abort
+ " Return true when negative index?
+ " let index = a:index >= 0 ? a:index : len(a:list) + a:index
+ return 0 <= a:index && a:index < len(a:list)
+endfunction
+
+" similar to Haskell's Data.List.span
+function! s:span(f, xs) abort
+ let border = len(a:xs)
+ for i in range(len(a:xs))
+ if !eval(substitute(a:f, 'v:val', string(a:xs[i]), 'g'))
+ let border = i
+ break
+ endif
+ endfor
+ return border == 0 ? [[], copy(a:xs)] : [a:xs[: border - 1], a:xs[border :]]
+endfunction
+
+" similar to Haskell's Data.List.break
+function! s:break(f, xs) abort
+ return s:span(printf('!(%s)', a:f), a:xs)
+endfunction
+
+" similar to Haskell's Data.List.takeWhile
+function! s:take_while(f, xs) abort
+ return s:span(a:f, a:xs)[0]
+endfunction
+
+" similar to Haskell's Data.List.partition
+function! s:partition(f, xs) abort
+ return [filter(copy(a:xs), a:f), filter(copy(a:xs), '!(' . a:f . ')')]
+endfunction
+
+" similar to Haskell's Prelude.all
+function! s:all(f, xs) abort
+ return !s:any(printf('!(%s)', a:f), a:xs)
+endfunction
+
+" similar to Haskell's Prelude.any
+function! s:any(f, xs) abort
+ return !empty(filter(map(copy(a:xs), a:f), 'v:val'))
+endfunction
+
+" similar to Haskell's Prelude.and
+function! s:and(xs) abort
+ return s:all('v:val', a:xs)
+endfunction
+
+" similar to Haskell's Prelude.or
+function! s:or(xs) abort
+ return s:any('v:val', a:xs)
+endfunction
+
+function! s:map_accum(expr, xs, init) abort
+ let memo = []
+ let init = a:init
+ for x in a:xs
+ let expr = substitute(a:expr, 'v:memo', init, 'g')
+ let expr = substitute(expr, 'v:val', x, 'g')
+ let [tmp, init] = eval(expr)
+ call add(memo, tmp)
+ endfor
+ return memo
+endfunction
+
+" similar to Haskell's Prelude.foldl
+function! s:foldl(f, init, xs) abort
+ let memo = a:init
+ for x in a:xs
+ let expr = substitute(a:f, 'v:val', string(x), 'g')
+ let expr = substitute(expr, 'v:memo', string(memo), 'g')
+ unlet memo
+ let memo = eval(expr)
+ endfor
+ return memo
+endfunction
+
+" similar to Haskell's Prelude.foldl1
+function! s:foldl1(f, xs) abort
+ if len(a:xs) == 0
+ throw 'vital: Data.List: foldl1'
+ endif
+ return s:foldl(a:f, a:xs[0], a:xs[1:])
+endfunction
+
+" similar to Haskell's Prelude.foldr
+function! s:foldr(f, init, xs) abort
+ return s:foldl(a:f, a:init, reverse(copy(a:xs)))
+endfunction
+
+" similar to Haskell's Prelude.fold11
+function! s:foldr1(f, xs) abort
+ if len(a:xs) == 0
+ throw 'vital: Data.List: foldr1'
+ endif
+ return s:foldr(a:f, a:xs[-1], a:xs[0:-2])
+endfunction
+
+" similar to python's zip()
+function! s:zip(...) abort
+ return map(range(min(map(copy(a:000), 'len(v:val)'))), "map(copy(a:000), 'v:val['.v:val.']')")
+endfunction
+
+" similar to zip(), but goes until the longer one.
+function! s:zip_fill(xs, ys, filler) abort
+ if empty(a:xs) && empty(a:ys)
+ return []
+ elseif empty(a:ys)
+ return s:cons([a:xs[0], a:filler], s:zip_fill(a:xs[1 :], [], a:filler))
+ elseif empty(a:xs)
+ return s:cons([a:filler, a:ys[0]], s:zip_fill([], a:ys[1 :], a:filler))
+ else
+ return s:cons([a:xs[0], a:ys[0]], s:zip_fill(a:xs[1 :], a:ys[1: ], a:filler))
+ endif
+endfunction
+
+" Inspired by Ruby's with_index method.
+function! s:with_index(list, ...) abort
+ let base = a:0 > 0 ? a:1 : 0
+ return map(copy(a:list), '[v:val, v:key + base]')
+endfunction
+
+" similar to Ruby's detect or Haskell's find.
+function! s:find(list, default, f) abort
+ for x in a:list
+ if eval(substitute(a:f, 'v:val', string(x), 'g'))
+ return x
+ endif
+ endfor
+ return a:default
+endfunction
+
+" Returns the index of the first element which satisfies the given expr.
+function! s:find_index(xs, f, ...) abort
+ let len = len(a:xs)
+ let start = a:0 > 0 ? (a:1 < 0 ? len + a:1 : a:1) : 0
+ let default = a:0 > 1 ? a:2 : -1
+ if start >=# len || start < 0
+ return default
+ endif
+ for i in range(start, len - 1)
+ if eval(substitute(a:f, 'v:val', string(a:xs[i]), 'g'))
+ return i
+ endif
+ endfor
+ return default
+endfunction
+
+" Returns the index of the last element which satisfies the given expr.
+function! s:find_last_index(xs, f, ...) abort
+ let len = len(a:xs)
+ let start = a:0 > 0 ? (a:1 < 0 ? len + a:1 : a:1) : len - 1
+ let default = a:0 > 1 ? a:2 : -1
+ if start >=# len || start < 0
+ return default
+ endif
+ for i in range(start, 0, -1)
+ if eval(substitute(a:f, 'v:val', string(a:xs[i]), 'g'))
+ return i
+ endif
+ endfor
+ return default
+endfunction
+
+" Similar to find_index but returns the list of indices satisfying the given expr.
+function! s:find_indices(xs, f, ...) abort
+ let len = len(a:xs)
+ let start = a:0 > 0 ? (a:1 < 0 ? len + a:1 : a:1) : 0
+ let result = []
+ if start >=# len || start < 0
+ return result
+ endif
+ for i in range(start, len - 1)
+ if eval(substitute(a:f, 'v:val', string(a:xs[i]), 'g'))
+ call add(result, i)
+ endif
+ endfor
+ return result
+endfunction
+
+" Return non-zero if a:list1 and a:list2 have any common item(s).
+" Return zero otherwise.
+function! s:has_common_items(list1, list2) abort
+ return !empty(filter(copy(a:list1), 'index(a:list2, v:val) isnot -1'))
+endfunction
+
+function! s:intersect(list1, list2) abort
+ let items = []
+ " for funcref
+ for X in a:list1
+ if index(a:list2, X) != -1 && index(items, X) == -1
+ let items += [X]
+ endif
+ endfor
+ return items
+endfunction
+
+" similar to Ruby's group_by.
+function! s:group_by(xs, f) abort
+ let result = {}
+ let list = map(copy(a:xs), printf('[v:val, %s]', a:f))
+ for x in list
+ let Val = x[0]
+ let key = type(x[1]) !=# type('') ? string(x[1]) : x[1]
+ if has_key(result, key)
+ call add(result[key], Val)
+ else
+ let result[key] = [Val]
+ endif
+ unlet Val
+ endfor
+ return result
+endfunction
+
+function! s:_default_compare(a, b) abort
+ return a:a <# a:b ? -1 : a:a ># a:b ? 1 : 0
+endfunction
+
+function! s:binary_search(list, value, ...) abort
+ let Predicate = a:0 >= 1 ? a:1 : 's:_default_compare'
+ let dic = a:0 >= 2 ? a:2 : {}
+ let start = 0
+ let end = len(a:list) - 1
+
+ while 1
+ if start > end
+ return -1
+ endif
+
+ let middle = (start + end) / 2
+
+ let compared = call(Predicate, [a:value, a:list[middle]], dic)
+
+ if compared < 0
+ let end = middle - 1
+ elseif compared > 0
+ let start = middle + 1
+ else
+ return middle
+ endif
+ endwhile
+endfunction
+
+function! s:product(lists) abort
+ let result = [[]]
+ for pool in a:lists
+ let tmp = []
+ for x in result
+ let tmp += map(copy(pool), 'x + [v:val]')
+ endfor
+ let result = tmp
+ endfor
+ return result
+endfunction
+
+function! s:permutations(list, ...) abort
+ if a:0 > 1
+ throw 'vital: Data.List: too many arguments'
+ endif
+ let r = a:0 == 1 ? a:1 : len(a:list)
+ if r > len(a:list)
+ return []
+ elseif r < 0
+ throw 'vital: Data.List: {r} must be non-negative integer'
+ endif
+ let n = len(a:list)
+ let result = []
+ for indices in s:product(map(range(r), 'range(n)'))
+ if len(s:uniq(indices)) == r
+ call add(result, map(indices, 'a:list[v:val]'))
+ endif
+ endfor
+ return result
+endfunction
+
+function! s:combinations(list, r) abort
+ if a:r > len(a:list)
+ return []
+ elseif a:r < 0
+ throw 'vital: Data:List: {r} must be non-negative integer'
+ endif
+ let n = len(a:list)
+ let result = []
+ for indices in s:permutations(range(n), a:r)
+ if s:sort(copy(indices), 'a:a - a:b') == indices
+ call add(result, map(indices, 'a:list[v:val]'))
+ endif
+ endfor
+ return result
+endfunction
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+" vim:set et ts=2 sts=2 sw=2 tw=0:
+
+endif
diff --git a/autoload/vital/_crystal/Data/String.vim b/autoload/vital/_crystal/Data/String.vim
new file mode 100644
index 00000000..c2e2382c
--- /dev/null
+++ b/autoload/vital/_crystal/Data/String.vim
@@ -0,0 +1,572 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+" Utilities for string.
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+function! s:_vital_loaded(V) abort
+ let s:V = a:V
+ let s:P = s:V.import('Prelude')
+ let s:L = s:V.import('Data.List')
+endfunction
+
+function! s:_vital_depends() abort
+ return ['Prelude', 'Data.List']
+endfunction
+
+" Substitute a:from => a:to by string.
+" To substitute by pattern, use substitute() instead.
+function! s:replace(str, from, to) abort
+ return s:_replace(a:str, a:from, a:to, 'g')
+endfunction
+
+" Substitute a:from => a:to only once.
+" cf. s:replace()
+function! s:replace_first(str, from, to) abort
+ return s:_replace(a:str, a:from, a:to, '')
+endfunction
+
+" implement of replace() and replace_first()
+function! s:_replace(str, from, to, flags) abort
+ return substitute(a:str, '\V'.escape(a:from, '\'), escape(a:to, '\'), a:flags)
+endfunction
+
+function! s:scan(str, pattern) abort
+ let list = []
+ call substitute(a:str, a:pattern, '\=add(list, submatch(0)) == [] ? "" : ""', 'g')
+ return list
+endfunction
+
+function! s:reverse(str) abort
+ return join(reverse(split(a:str, '.\zs')), '')
+endfunction
+
+function! s:starts_with(str, prefix) abort
+ return stridx(a:str, a:prefix) == 0
+endfunction
+
+function! s:ends_with(str, suffix) abort
+ let idx = strridx(a:str, a:suffix)
+ return 0 <= idx && idx + len(a:suffix) == len(a:str)
+endfunction
+
+function! s:common_head(strs) abort
+ if empty(a:strs)
+ return ''
+ endif
+ let len = len(a:strs)
+ if len == 1
+ return a:strs[0]
+ endif
+ let strs = len == 2 ? a:strs : sort(copy(a:strs))
+ let pat = substitute(strs[0], '.', '\="[" . escape(submatch(0), "^\\") . "]"', 'g')
+ return pat == '' ? '' : matchstr(strs[-1], '\C^\%[' . pat . ']')
+endfunction
+
+" Split to two elements of List. ([left, right])
+" e.g.: s:split3('neocomplcache', 'compl') returns ['neo', 'compl', 'cache']
+function! s:split_leftright(expr, pattern) abort
+ let [left, _, right] = s:split3(a:expr, a:pattern)
+ return [left, right]
+endfunction
+
+function! s:split3(expr, pattern) abort
+ let ERROR = ['', '', '']
+ if a:expr ==# '' || a:pattern ==# ''
+ return ERROR
+ endif
+ let begin = match(a:expr, a:pattern)
+ if begin is -1
+ return ERROR
+ endif
+ let end = matchend(a:expr, a:pattern)
+ let left = begin <=# 0 ? '' : a:expr[: begin - 1]
+ let right = a:expr[end :]
+ return [left, a:expr[begin : end-1], right]
+endfunction
+
+" Slices into strings determines the number of substrings.
+" e.g.: s:nsplit("neo compl cache", 2, '\s') returns ['neo', 'compl cache']
+function! s:nsplit(expr, n, ...) abort
+ let pattern = get(a:000, 0, '\s')
+ let keepempty = get(a:000, 1, 1)
+ let ret = []
+ let expr = a:expr
+ if a:n <= 1
+ return [expr]
+ endif
+ while 1
+ let pos = match(expr, pattern)
+ if pos == -1
+ if expr !~ pattern || keepempty
+ call add(ret, expr)
+ endif
+ break
+ elseif pos >= 0
+ let left = pos > 0 ? expr[:pos-1] : ''
+ if pos > 0 || keepempty
+ call add(ret, left)
+ endif
+ let ml = len(matchstr(expr, pattern))
+ if pos == 0 && ml == 0
+ let pos = 1
+ endif
+ let expr = expr[pos+ml :]
+ endif
+ if len(expr) == 0
+ break
+ endif
+ if len(ret) == a:n - 1
+ call add(ret, expr)
+ break
+ endif
+ endwhile
+ return ret
+endfunction
+
+" Returns the number of character in a:str.
+" NOTE: This returns proper value
+" even if a:str contains multibyte character(s).
+" s:strchars(str) {{{
+if exists('*strchars')
+ function! s:strchars(str) abort
+ return strchars(a:str)
+ endfunction
+else
+ function! s:strchars(str) abort
+ return strlen(substitute(copy(a:str), '.', 'x', 'g'))
+ endfunction
+endif "}}}
+
+" Returns the bool of contains any multibyte character in s:str
+function! s:contains_multibyte(str) abort "{{{
+ return strlen(a:str) != s:strchars(a:str)
+endfunction "}}}
+
+" Remove last character from a:str.
+" NOTE: This returns proper value
+" even if a:str contains multibyte character(s).
+function! s:chop(str) abort "{{{
+ return substitute(a:str, '.$', '', '')
+endfunction "}}}
+
+" Remove last \r,\n,\r\n from a:str.
+function! s:chomp(str) abort "{{{
+ return substitute(a:str, '\%(\r\n\|[\r\n]\)$', '', '')
+endfunction "}}}
+
+" wrap() and its internal functions
+" * _split_by_wcswidth_once()
+" * _split_by_wcswidth()
+" * _concat()
+" * wrap()
+"
+" NOTE _concat() is just a copy of Data.List.concat().
+" FIXME don't repeat yourself
+function! s:_split_by_wcswidth_once(body, x) abort
+ let fst = s:strwidthpart(a:body, a:x)
+ let snd = s:strwidthpart_reverse(a:body, s:wcswidth(a:body) - s:wcswidth(fst))
+ return [fst, snd]
+endfunction
+
+function! s:_split_by_wcswidth(body, x) abort
+ let memo = []
+ let body = a:body
+ while s:wcswidth(body) > a:x
+ let [tmp, body] = s:_split_by_wcswidth_once(body, a:x)
+ call add(memo, tmp)
+ endwhile
+ call add(memo, body)
+ return memo
+endfunction
+
+function! s:trim(str) abort
+ return matchstr(a:str,'^\s*\zs.\{-}\ze\s*$')
+endfunction
+
+function! s:trim_start(str) abort
+ return matchstr(a:str,'^\s*\zs.\{-}$')
+endfunction
+
+function! s:trim_end(str) abort
+ return matchstr(a:str,'^.\{-}\ze\s*$')
+endfunction
+
+function! s:wrap(str,...) abort
+ let _columns = a:0 > 0 ? a:1 : &columns
+ return s:L.concat(
+ \ map(split(a:str, '\r\n\|[\r\n]'), 's:_split_by_wcswidth(v:val, _columns - 1)'))
+endfunction
+
+function! s:nr2byte(nr) abort
+ if a:nr < 0x80
+ return nr2char(a:nr)
+ elseif a:nr < 0x800
+ return nr2char(a:nr/64+192).nr2char(a:nr%64+128)
+ else
+ return nr2char(a:nr/4096%16+224).nr2char(a:nr/64%64+128).nr2char(a:nr%64+128)
+ endif
+endfunction
+
+function! s:nr2enc_char(charcode) abort
+ if &encoding == 'utf-8'
+ return nr2char(a:charcode)
+ endif
+ let char = s:nr2byte(a:charcode)
+ if strlen(char) > 1
+ let char = strtrans(iconv(char, 'utf-8', &encoding))
+ endif
+ return char
+endfunction
+
+function! s:nr2hex(nr) abort
+ let n = a:nr
+ let r = ""
+ while n
+ let r = '0123456789ABCDEF'[n % 16] . r
+ let n = n / 16
+ endwhile
+ return r
+endfunction
+
+" If a ==# b, returns -1.
+" If a !=# b, returns first index of different character.
+function! s:diffidx(a, b) abort
+ return a:a ==# a:b ? -1 : strlen(s:common_head([a:a, a:b]))
+endfunction
+
+function! s:substitute_last(expr, pat, sub) abort
+ return substitute(a:expr, printf('.*\zs%s', a:pat), a:sub, '')
+endfunction
+
+function! s:dstring(expr) abort
+ let x = substitute(string(a:expr), "^'\\|'$", '', 'g')
+ let x = substitute(x, "''", "'", 'g')
+ return printf('"%s"', escape(x, '"'))
+endfunction
+
+function! s:lines(str) abort
+ return split(a:str, '\r\?\n')
+endfunction
+
+function! s:_pad_with_char(str, left, right, char) abort
+ return repeat(a:char, a:left). a:str. repeat(a:char, a:right)
+endfunction
+
+function! s:pad_left(str, width, ...) abort
+ let char = get(a:, 1, ' ')
+ if strdisplaywidth(char) != 1
+ throw "vital: Data.String: Can't use non-half-width characters for padding."
+ endif
+ let left = max([0, a:width - strdisplaywidth(a:str)])
+ return s:_pad_with_char(a:str, left, 0, char)
+endfunction
+
+function! s:pad_right(str, width, ...) abort
+ let char = get(a:, 1, ' ')
+ if strdisplaywidth(char) != 1
+ throw "vital: Data.String: Can't use non-half-width characters for padding."
+ endif
+ let right = max([0, a:width - strdisplaywidth(a:str)])
+ return s:_pad_with_char(a:str, 0, right, char)
+endfunction
+
+function! s:pad_both_sides(str, width, ...) abort
+ let char = get(a:, 1, ' ')
+ if strdisplaywidth(char) != 1
+ throw "vital: Data.String: Can't use non-half-width characters for padding."
+ endif
+ let space = max([0, a:width - strdisplaywidth(a:str)])
+ let left = space / 2
+ let right = space - left
+ return s:_pad_with_char(a:str, left, right, char)
+endfunction
+
+function! s:pad_between_letters(str, width, ...) abort
+ let char = get(a:, 1, ' ')
+ if strdisplaywidth(char) != 1
+ throw "vital: Data.String: Can't use non-half-width characters for padding."
+ endif
+ let letters = split(a:str, '\zs')
+ let each_width = a:width / len(letters)
+ let str = join(map(letters, 's:pad_both_sides(v:val, each_width, char)'), '')
+ if a:width - strdisplaywidth(str) > 0
+ return char. s:pad_both_sides(str, a:width - 1, char)
+ endif
+ return str
+endfunction
+
+function! s:justify_equal_spacing(str, width, ...) abort
+ let char = get(a:, 1, ' ')
+ if strdisplaywidth(char) != 1
+ throw "vital: Data.String: Can't use non-half-width characters for padding."
+ endif
+ let letters = split(a:str, '\zs')
+ let first_letter = letters[0]
+ " {width w/o the first letter} / {length w/o the first letter}
+ let each_width = (a:width - strdisplaywidth(first_letter)) / (len(letters) - 1)
+ let remainder = (a:width - strdisplaywidth(first_letter)) % (len(letters) - 1)
+ return first_letter. join(s:L.concat([
+\ map(letters[1:remainder], 's:pad_left(v:val, each_width + 1, char)'),
+\ map(letters[remainder + 1:], 's:pad_left(v:val, each_width, char)')
+\ ]), '')
+endfunction
+
+function! s:levenshtein_distance(str1, str2) abort
+ let letters1 = split(a:str1, '\zs')
+ let letters2 = split(a:str2, '\zs')
+ let length1 = len(letters1)
+ let length2 = len(letters2)
+ let distances = map(range(1, length1 + 1), 'map(range(1, length2 + 1), "0")')
+
+ for i1 in range(0, length1)
+ let distances[i1][0] = i1
+ endfor
+ for i2 in range(0, length2)
+ let distances[0][i2] = i2
+ endfor
+
+ for i1 in range(1, length1)
+ for i2 in range(1, length2)
+ let cost = (letters1[i1 - 1] ==# letters2[i2 - 1]) ? 0 : 1
+
+ let distances[i1][i2] = min([
+ \ distances[i1 - 1][i2 ] + 1,
+ \ distances[i1 ][i2 - 1] + 1,
+ \ distances[i1 - 1][i2 - 1] + cost,
+ \])
+ endfor
+ endfor
+
+ return distances[length1][length2]
+endfunction
+
+function! s:padding_by_displaywidth(expr, width, float) abort
+ let padding_char = ' '
+ let n = a:width - strdisplaywidth(a:expr)
+ if n <= 0
+ let n = 0
+ endif
+ if a:float < 0
+ return a:expr . repeat(padding_char, n)
+ elseif 0 < a:float
+ return repeat(padding_char, n) . a:expr
+ else
+ if n % 2 is 0
+ return repeat(padding_char, n / 2) . a:expr . repeat(padding_char, n / 2)
+ else
+ return repeat(padding_char, (n - 1) / 2) . a:expr . repeat(padding_char, (n - 1) / 2) . padding_char
+ endif
+ endif
+endfunction
+
+function! s:split_by_displaywidth(expr, width, float, is_wrap) abort
+ if a:width is 0
+ return ['']
+ endif
+
+ let lines = []
+
+ let cs = split(a:expr, '\zs')
+ let cs_index = 0
+
+ let text = ''
+ while cs_index < len(cs)
+ if cs[cs_index] is "\n"
+ let text = s:padding_by_displaywidth(text, a:width, a:float)
+ let lines += [text]
+ let text = ''
+ else
+ let w = strdisplaywidth(text . cs[cs_index])
+
+ if w < a:width
+ let text .= cs[cs_index]
+ elseif a:width < w
+ let text = s:padding_by_displaywidth(text, a:width, a:float)
+ else
+ let text .= cs[cs_index]
+ endif
+
+ if a:width <= w
+ let lines += [text]
+ let text = ''
+ if a:is_wrap
+ if a:width < w
+ if a:width < strdisplaywidth(cs[cs_index])
+ while get(cs, cs_index, "\n") isnot "\n"
+ let cs_index += 1
+ endwhile
+ continue
+ else
+ let text = cs[cs_index]
+ endif
+ endif
+ else
+ while get(cs, cs_index, "\n") isnot "\n"
+ let cs_index += 1
+ endwhile
+ continue
+ endif
+ endif
+
+ endif
+ let cs_index += 1
+ endwhile
+
+ if !empty(text)
+ let lines += [ s:padding_by_displaywidth(text, a:width, a:float) ]
+ endif
+
+ return lines
+endfunction
+
+function! s:hash(str) abort
+ if exists('*sha256')
+ return sha256(a:str)
+ else
+ " This gives up sha256ing but just adds up char with index.
+ let sum = 0
+ for i in range(len(a:str))
+ let sum += char2nr(a:str[i]) * (i + 1)
+ endfor
+
+ return printf('%x', sum)
+ endif
+endfunction
+
+function! s:truncate(str, width) abort
+ " Original function is from mattn.
+ " http://github.com/mattn/googlereader-vim/tree/master
+
+ if a:str =~# '^[\x00-\x7f]*$'
+ return len(a:str) < a:width ?
+ \ printf('%-'.a:width.'s', a:str) : strpart(a:str, 0, a:width)
+ endif
+
+ let ret = a:str
+ let width = s:wcswidth(a:str)
+ if width > a:width
+ let ret = s:strwidthpart(ret, a:width)
+ let width = s:wcswidth(ret)
+ endif
+
+ if width < a:width
+ let ret .= repeat(' ', a:width - width)
+ endif
+
+ return ret
+endfunction
+
+function! s:truncate_skipping(str, max, footer_width, separator) abort
+ let width = s:wcswidth(a:str)
+ if width <= a:max
+ let ret = a:str
+ else
+ let header_width = a:max - s:wcswidth(a:separator) - a:footer_width
+ let ret = s:strwidthpart(a:str, header_width) . a:separator
+ \ . s:strwidthpart_reverse(a:str, a:footer_width)
+ endif
+ return s:truncate(ret, a:max)
+endfunction
+
+function! s:strwidthpart(str, width) abort
+ if a:width <= 0
+ return ''
+ endif
+ let strarr = split(a:str, '\zs')
+ let width = s:wcswidth(a:str)
+ let index = len(strarr)
+ let diff = (index + 1) / 2
+ let rightindex = index - 1
+ while width > a:width
+ let index = max([rightindex - diff + 1, 0])
+ let partwidth = s:wcswidth(join(strarr[(index):(rightindex)], ''))
+ if width - partwidth >= a:width || diff <= 1
+ let width -= partwidth
+ let rightindex = index - 1
+ endif
+ if diff > 1
+ let diff = diff / 2
+ endif
+ endwhile
+ return index ? join(strarr[:index - 1], '') : ''
+endfunction
+
+function! s:strwidthpart_reverse(str, width) abort
+ if a:width <= 0
+ return ''
+ endif
+ let strarr = split(a:str, '\zs')
+ let width = s:wcswidth(a:str)
+ let strlen = len(strarr)
+ let diff = (strlen + 1) / 2
+ let leftindex = 0
+ let index = -1
+ while width > a:width
+ let index = min([leftindex + diff, strlen]) - 1
+ let partwidth = s:wcswidth(join(strarr[(leftindex):(index)], ''))
+ if width - partwidth >= a:width || diff <= 1
+ let width -= partwidth
+ let leftindex = index + 1
+ endif
+ if diff > 1
+ let diff = diff / 2
+ endif
+ endwhile
+ return index < strlen ? join(strarr[(index + 1):], '') : ''
+endfunction
+
+if v:version >= 703
+ " Use builtin function.
+ function! s:wcswidth(str) abort
+ return strwidth(a:str)
+ endfunction
+else
+ function! s:wcswidth(str) abort
+ if a:str =~# '^[\x00-\x7f]*$'
+ return strlen(a:str)
+ endif
+ let mx_first = '^\(.\)'
+ let str = a:str
+ let width = 0
+ while 1
+ let ucs = char2nr(substitute(str, mx_first, '\1', ''))
+ if ucs == 0
+ break
+ endif
+ let width += s:_wcwidth(ucs)
+ let str = substitute(str, mx_first, '', '')
+ endwhile
+ return width
+ endfunction
+
+ " UTF-8 only.
+ function! s:_wcwidth(ucs) abort
+ let ucs = a:ucs
+ if (ucs >= 0x1100
+ \ && (ucs <= 0x115f
+ \ || ucs == 0x2329
+ \ || ucs == 0x232a
+ \ || (ucs >= 0x2e80 && ucs <= 0xa4cf
+ \ && ucs != 0x303f)
+ \ || (ucs >= 0xac00 && ucs <= 0xd7a3)
+ \ || (ucs >= 0xf900 && ucs <= 0xfaff)
+ \ || (ucs >= 0xfe30 && ucs <= 0xfe6f)
+ \ || (ucs >= 0xff00 && ucs <= 0xff60)
+ \ || (ucs >= 0xffe0 && ucs <= 0xffe6)
+ \ || (ucs >= 0x20000 && ucs <= 0x2fffd)
+ \ || (ucs >= 0x30000 && ucs <= 0x3fffd)
+ \ ))
+ return 2
+ endif
+ return 1
+ endfunction
+endif
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+" vim:set et ts=2 sts=2 sw=2 tw=0:
+
+endif
diff --git a/autoload/vital/_crystal/Prelude.vim b/autoload/vital/_crystal/Prelude.vim
new file mode 100644
index 00000000..be31f980
--- /dev/null
+++ b/autoload/vital/_crystal/Prelude.vim
@@ -0,0 +1,389 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+if v:version ># 703 ||
+\ (v:version is 703 && has('patch465'))
+ function! s:glob(expr) abort
+ return glob(a:expr, 1, 1)
+ endfunction
+else
+ function! s:glob(expr) abort
+ let R = glob(a:expr, 1)
+ return split(R, '\n')
+ endfunction
+endif
+
+function! s:globpath(path, expr) abort
+ let R = globpath(a:path, a:expr, 1)
+ return split(R, '\n')
+endfunction
+
+" Wrapper functions for type().
+let [
+\ s:__TYPE_NUMBER,
+\ s:__TYPE_STRING,
+\ s:__TYPE_FUNCREF,
+\ s:__TYPE_LIST,
+\ s:__TYPE_DICT,
+\ s:__TYPE_FLOAT] = [
+ \ type(3),
+ \ type(""),
+ \ type(function('tr')),
+ \ type([]),
+ \ type({}),
+ \ has('float') ? type(str2float('0')) : -1]
+" __TYPE_FLOAT = -1 when -float
+" This doesn't match to anything.
+
+" Number or Float
+function! s:is_numeric(Value) abort
+ let _ = type(a:Value)
+ return _ ==# s:__TYPE_NUMBER
+ \ || _ ==# s:__TYPE_FLOAT
+endfunction
+
+" Number
+function! s:is_number(Value) abort
+ return type(a:Value) ==# s:__TYPE_NUMBER
+endfunction
+
+" Float
+function! s:is_float(Value) abort
+ return type(a:Value) ==# s:__TYPE_FLOAT
+endfunction
+" String
+function! s:is_string(Value) abort
+ return type(a:Value) ==# s:__TYPE_STRING
+endfunction
+" Funcref
+function! s:is_funcref(Value) abort
+ return type(a:Value) ==# s:__TYPE_FUNCREF
+endfunction
+" List
+function! s:is_list(Value) abort
+ return type(a:Value) ==# s:__TYPE_LIST
+endfunction
+" Dictionary
+function! s:is_dict(Value) abort
+ return type(a:Value) ==# s:__TYPE_DICT
+endfunction
+
+function! s:truncate_skipping(str, max, footer_width, separator) abort
+ call s:_warn_deprecated("truncate_skipping", "Data.String.truncate_skipping")
+
+ let width = s:wcswidth(a:str)
+ if width <= a:max
+ let ret = a:str
+ else
+ let header_width = a:max - s:wcswidth(a:separator) - a:footer_width
+ let ret = s:strwidthpart(a:str, header_width) . a:separator
+ \ . s:strwidthpart_reverse(a:str, a:footer_width)
+ endif
+
+ return s:truncate(ret, a:max)
+endfunction
+
+function! s:truncate(str, width) abort
+ " Original function is from mattn.
+ " http://github.com/mattn/googlereader-vim/tree/master
+
+ call s:_warn_deprecated("truncate", "Data.String.truncate")
+
+ if a:str =~# '^[\x00-\x7f]*$'
+ return len(a:str) < a:width ?
+ \ printf('%-'.a:width.'s', a:str) : strpart(a:str, 0, a:width)
+ endif
+
+ let ret = a:str
+ let width = s:wcswidth(a:str)
+ if width > a:width
+ let ret = s:strwidthpart(ret, a:width)
+ let width = s:wcswidth(ret)
+ endif
+
+ if width < a:width
+ let ret .= repeat(' ', a:width - width)
+ endif
+
+ return ret
+endfunction
+
+function! s:strwidthpart(str, width) abort
+ call s:_warn_deprecated("strwidthpart", "Data.String.strwidthpart")
+
+ if a:width <= 0
+ return ''
+ endif
+ let ret = a:str
+ let width = s:wcswidth(a:str)
+ while width > a:width
+ let char = matchstr(ret, '.$')
+ let ret = ret[: -1 - len(char)]
+ let width -= s:wcswidth(char)
+ endwhile
+
+ return ret
+endfunction
+function! s:strwidthpart_reverse(str, width) abort
+ call s:_warn_deprecated("strwidthpart_reverse", "Data.String.strwidthpart_reverse")
+
+ if a:width <= 0
+ return ''
+ endif
+ let ret = a:str
+ let width = s:wcswidth(a:str)
+ while width > a:width
+ let char = matchstr(ret, '^.')
+ let ret = ret[len(char) :]
+ let width -= s:wcswidth(char)
+ endwhile
+
+ return ret
+endfunction
+
+if v:version >= 703
+ " Use builtin function.
+ function! s:wcswidth(str) abort
+ call s:_warn_deprecated("wcswidth", "Data.String.wcswidth")
+ return strwidth(a:str)
+ endfunction
+else
+ function! s:wcswidth(str) abort
+ call s:_warn_deprecated("wcswidth", "Data.String.wcswidth")
+
+ if a:str =~# '^[\x00-\x7f]*$'
+ return strlen(a:str)
+ end
+
+ let mx_first = '^\(.\)'
+ let str = a:str
+ let width = 0
+ while 1
+ let ucs = char2nr(substitute(str, mx_first, '\1', ''))
+ if ucs == 0
+ break
+ endif
+ let width += s:_wcwidth(ucs)
+ let str = substitute(str, mx_first, '', '')
+ endwhile
+ return width
+ endfunction
+
+ " UTF-8 only.
+ function! s:_wcwidth(ucs) abort
+ let ucs = a:ucs
+ if (ucs >= 0x1100
+ \ && (ucs <= 0x115f
+ \ || ucs == 0x2329
+ \ || ucs == 0x232a
+ \ || (ucs >= 0x2e80 && ucs <= 0xa4cf
+ \ && ucs != 0x303f)
+ \ || (ucs >= 0xac00 && ucs <= 0xd7a3)
+ \ || (ucs >= 0xf900 && ucs <= 0xfaff)
+ \ || (ucs >= 0xfe30 && ucs <= 0xfe6f)
+ \ || (ucs >= 0xff00 && ucs <= 0xff60)
+ \ || (ucs >= 0xffe0 && ucs <= 0xffe6)
+ \ || (ucs >= 0x20000 && ucs <= 0x2fffd)
+ \ || (ucs >= 0x30000 && ucs <= 0x3fffd)
+ \ ))
+ return 2
+ endif
+ return 1
+ endfunction
+endif
+
+let s:is_windows = has('win16') || has('win32') || has('win64') || has('win95')
+let s:is_cygwin = has('win32unix')
+let s:is_mac = !s:is_windows && !s:is_cygwin
+ \ && (has('mac') || has('macunix') || has('gui_macvim') ||
+ \ (!isdirectory('/proc') && executable('sw_vers')))
+let s:is_unix = has('unix')
+
+function! s:is_windows() abort
+ return s:is_windows
+endfunction
+
+function! s:is_cygwin() abort
+ return s:is_cygwin
+endfunction
+
+function! s:is_mac() abort
+ return s:is_mac
+endfunction
+
+function! s:is_unix() abort
+ return s:is_unix
+endfunction
+
+function! s:_warn_deprecated(name, alternative) abort
+ try
+ echohl Error
+ echomsg "Prelude." . a:name . " is deprecated! Please use " . a:alternative . " instead."
+ finally
+ echohl None
+ endtry
+endfunction
+
+function! s:smart_execute_command(action, word) abort
+ execute a:action . ' ' . (a:word == '' ? '' : '`=a:word`')
+endfunction
+
+function! s:escape_file_searching(buffer_name) abort
+ return escape(a:buffer_name, '*[]?{}, ')
+endfunction
+
+function! s:escape_pattern(str) abort
+ return escape(a:str, '~"\.^$[]*')
+endfunction
+
+function! s:getchar(...) abort
+ let c = call('getchar', a:000)
+ return type(c) == type(0) ? nr2char(c) : c
+endfunction
+
+function! s:getchar_safe(...) abort
+ let c = s:input_helper('getchar', a:000)
+ return type(c) == type("") ? c : nr2char(c)
+endfunction
+
+function! s:input_safe(...) abort
+ return s:input_helper('input', a:000)
+endfunction
+
+function! s:input_helper(funcname, args) abort
+ let success = 0
+ if inputsave() !=# success
+ throw 'vital: Prelude: inputsave() failed'
+ endif
+ try
+ return call(a:funcname, a:args)
+ finally
+ if inputrestore() !=# success
+ throw 'vital: Prelude: inputrestore() failed'
+ endif
+ endtry
+endfunction
+
+function! s:set_default(var, val) abort
+ if !exists(a:var) || type({a:var}) != type(a:val)
+ let {a:var} = a:val
+ endif
+endfunction
+
+function! s:substitute_path_separator(path) abort
+ return s:is_windows ? substitute(a:path, '\\', '/', 'g') : a:path
+endfunction
+
+function! s:path2directory(path) abort
+ return s:substitute_path_separator(isdirectory(a:path) ? a:path : fnamemodify(a:path, ':p:h'))
+endfunction
+
+function! s:_path2project_directory_git(path) abort
+ let parent = a:path
+
+ while 1
+ let path = parent . '/.git'
+ if isdirectory(path) || filereadable(path)
+ return parent
+ endif
+ let next = fnamemodify(parent, ':h')
+ if next == parent
+ return ''
+ endif
+ let parent = next
+ endwhile
+endfunction
+
+function! s:_path2project_directory_svn(path) abort
+ let search_directory = a:path
+ let directory = ''
+
+ let find_directory = s:escape_file_searching(search_directory)
+ let d = finddir('.svn', find_directory . ';')
+ if d == ''
+ return ''
+ endif
+
+ let directory = fnamemodify(d, ':p:h:h')
+
+ " Search parent directories.
+ let parent_directory = s:path2directory(
+ \ fnamemodify(directory, ':h'))
+
+ if parent_directory != ''
+ let d = finddir('.svn', parent_directory . ';')
+ if d != ''
+ let directory = s:_path2project_directory_svn(parent_directory)
+ endif
+ endif
+ return directory
+endfunction
+
+function! s:_path2project_directory_others(vcs, path) abort
+ let vcs = a:vcs
+ let search_directory = a:path
+
+ let find_directory = s:escape_file_searching(search_directory)
+ let d = finddir(vcs, find_directory . ';')
+ if d == ''
+ return ''
+ endif
+ return fnamemodify(d, ':p:h:h')
+endfunction
+
+function! s:path2project_directory(path, ...) abort
+ let is_allow_empty = get(a:000, 0, 0)
+ let search_directory = s:path2directory(a:path)
+ let directory = ''
+
+ " Search VCS directory.
+ for vcs in ['.git', '.bzr', '.hg', '.svn']
+ if vcs ==# '.git'
+ let directory = s:_path2project_directory_git(search_directory)
+ elseif vcs ==# '.svn'
+ let directory = s:_path2project_directory_svn(search_directory)
+ else
+ let directory = s:_path2project_directory_others(vcs, search_directory)
+ endif
+ if directory != ''
+ break
+ endif
+ endfor
+
+ " Search project file.
+ if directory == ''
+ for d in ['build.xml', 'prj.el', '.project', 'pom.xml', 'package.json',
+ \ 'Makefile', 'configure', 'Rakefile', 'NAnt.build',
+ \ 'P4CONFIG', 'tags', 'gtags']
+ let d = findfile(d, s:escape_file_searching(search_directory) . ';')
+ if d != ''
+ let directory = fnamemodify(d, ':p:h')
+ break
+ endif
+ endfor
+ endif
+
+ if directory == ''
+ " Search /src/ directory.
+ let base = s:substitute_path_separator(search_directory)
+ if base =~# '/src/'
+ let directory = base[: strridx(base, '/src/') + 3]
+ endif
+ endif
+
+ if directory == '' && !is_allow_empty
+ " Use original path.
+ let directory = search_directory
+ endif
+
+ return s:substitute_path_separator(directory)
+endfunction
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+" vim:set et ts=2 sts=2 sw=2 tw=0:
+
+endif
diff --git a/autoload/vital/_crystal/Process.vim b/autoload/vital/_crystal/Process.vim
new file mode 100644
index 00000000..c2deb9e5
--- /dev/null
+++ b/autoload/vital/_crystal/Process.vim
@@ -0,0 +1,185 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+" TODO: move all comments to doc file.
+"
+"
+" FIXME: This module name should be Vital.System ?
+" But the name has been already taken.
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+
+" FIXME: Unfortunately, can't use s:_vital_loaded() for this purpose.
+" Because these variables are used when this script file is loaded.
+let s:is_windows = has('win16') || has('win32') || has('win64') || has('win95')
+let s:is_unix = has('unix')
+" As of 7.4.122, the system()'s 1st argument is converted internally by Vim.
+" Note that Patch 7.4.122 does not convert system()'s 2nd argument and
+" return-value. We must convert them manually.
+let s:need_trans = v:version < 704 || (v:version == 704 && !has('patch122'))
+
+let s:TYPE_DICT = type({})
+let s:TYPE_LIST = type([])
+let s:TYPE_STRING = type("")
+
+
+" Execute program in the background from Vim.
+" Return an empty string always.
+"
+" If a:expr is a List, shellescape() each argument.
+" If a:expr is a String, the arguments are passed as-is.
+"
+" Windows:
+" Using :!start , execute program without via cmd.exe.
+" Spawning 'expr' with 'noshellslash'
+" keep special characters from unwanted expansion.
+" (see :help shellescape())
+"
+" Unix:
+" using :! , execute program in the background by shell.
+function! s:spawn(expr, ...) abort
+ let shellslash = 0
+ if s:is_windows
+ let shellslash = &l:shellslash
+ setlocal noshellslash
+ endif
+ try
+ if type(a:expr) is s:TYPE_LIST
+ let special = 1
+ let cmdline = join(map(a:expr, 'shellescape(v:val, special)'), ' ')
+ elseif type(a:expr) is s:TYPE_STRING
+ let cmdline = a:expr
+ if a:0 && a:1
+ " for :! command
+ let cmdline = substitute(cmdline, '\([!%#]\|<[^<>]\+>\)', '\\\1', 'g')
+ endif
+ else
+ throw 'Process.spawn(): invalid argument (value type:'.type(a:expr).')'
+ endif
+ if s:is_windows
+ silent execute '!start' cmdline
+ else
+ silent execute '!' cmdline '&'
+ endif
+ finally
+ if s:is_windows
+ let &l:shellslash = shellslash
+ endif
+ endtry
+ return ''
+endfunction
+
+" iconv() wrapper for safety.
+function! s:iconv(expr, from, to) abort
+ if a:from == '' || a:to == '' || a:from ==? a:to
+ return a:expr
+ endif
+ let result = iconv(a:expr, a:from, a:to)
+ return result != '' ? result : a:expr
+endfunction
+
+" Check vimproc.
+function! s:has_vimproc() abort
+ if !exists('s:exists_vimproc')
+ try
+ call vimproc#version()
+ let s:exists_vimproc = 1
+ catch
+ let s:exists_vimproc = 0
+ endtry
+ endif
+ return s:exists_vimproc
+endfunction
+
+" * {command} [, {input} [, {timeout}]]
+" * {command} [, {dict}]
+" {dict} = {
+" use_vimproc: bool,
+" input: string,
+" timeout: bool,
+" background: bool,
+" }
+function! s:system(str, ...) abort
+ " Process optional arguments at first
+ " because use_vimproc is required later
+ " for a:str argument.
+ let input = ''
+ let use_vimproc = s:has_vimproc()
+ let background = 0
+ let args = []
+ if a:0 ==# 1
+ " {command} [, {dict}]
+ " a:1 = {dict}
+ if type(a:1) is s:TYPE_DICT
+ if has_key(a:1, 'use_vimproc')
+ let use_vimproc = a:1.use_vimproc
+ endif
+ if has_key(a:1, 'input')
+ let args += [s:iconv(a:1.input, &encoding, 'char')]
+ endif
+ if use_vimproc && has_key(a:1, 'timeout')
+ " ignores timeout unless you have vimproc.
+ let args += [a:1.timeout]
+ endif
+ if has_key(a:1, 'background')
+ let background = a:1.background
+ endif
+ elseif type(a:1) is s:TYPE_STRING
+ let args += [s:iconv(a:1, &encoding, 'char')]
+ else
+ throw 'Process.system(): invalid argument (value type:'.type(a:1).')'
+ endif
+ elseif a:0 >= 2
+ " {command} [, {input} [, {timeout}]]
+ " a:000 = [{input} [, {timeout}]]
+ let [input; rest] = a:000
+ let input = s:iconv(input, &encoding, 'char')
+ let args += [input] + rest
+ endif
+
+ " Process a:str argument.
+ if type(a:str) is s:TYPE_LIST
+ let expr = use_vimproc ? '"''" . v:val . "''"' : 's:shellescape(v:val)'
+ let command = join(map(copy(a:str), expr), ' ')
+ elseif type(a:str) is s:TYPE_STRING
+ let command = a:str
+ else
+ throw 'Process.system(): invalid argument (value type:'.type(a:str).')'
+ endif
+ if s:need_trans
+ let command = s:iconv(command, &encoding, 'char')
+ endif
+ let args = [command] + args
+ if background && (use_vimproc || !s:is_windows)
+ let args[0] = args[0] . ' &'
+ endif
+
+ let funcname = use_vimproc ? 'vimproc#system' : 'system'
+ let output = call(funcname, args)
+ let output = s:iconv(output, 'char', &encoding)
+ return output
+endfunction
+
+function! s:get_last_status() abort
+ return s:has_vimproc() ?
+ \ vimproc#get_last_status() : v:shell_error
+endfunction
+
+if s:is_windows
+ function! s:shellescape(command) abort
+ return substitute(a:command, '[&()[\]{}^=;!''+,`~]', '^\0', 'g')
+ endfunction
+else
+ function! s:shellescape(...) abort
+ return call('shellescape', a:000)
+ endfunction
+endif
+
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+" vim:set et ts=2 sts=2 sw=2 tw=0:
+
+endif
diff --git a/autoload/vital/_crystal/Web/JSON.vim b/autoload/vital/_crystal/Web/JSON.vim
new file mode 100644
index 00000000..0e42ee0d
--- /dev/null
+++ b/autoload/vital/_crystal/Web/JSON.vim
@@ -0,0 +1,112 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+function! s:_true() abort
+ return 1
+endfunction
+
+function! s:_false() abort
+ return 0
+endfunction
+
+function! s:_null() abort
+ return 0
+endfunction
+
+let s:const = {}
+let s:const.true = function('s:_true')
+let s:const.false = function('s:_false')
+let s:const.null = function('s:_null')
+
+function! s:_resolve(val, prefix) abort
+ let t = type(a:val)
+ if t == type('')
+ let m = matchlist(a:val, '^' . a:prefix . '\(null\|true\|false\)$')
+ if !empty(m)
+ return s:const[m[1]]
+ endif
+ elseif t == type([]) || t == type({})
+ return map(a:val, 's:_resolve(v:val, a:prefix)')
+ endif
+ return a:val
+endfunction
+
+
+function! s:_vital_created(module) abort
+ " define constant variables
+ call extend(a:module, s:const)
+endfunction
+
+function! s:_vital_loaded(V) abort
+ let s:V = a:V
+ let s:string = s:V.import('Data.String')
+endfunction
+
+function! s:_vital_depends() abort
+ return ['Data.String']
+endfunction
+
+" @vimlint(EVL102, 1, l:null)
+" @vimlint(EVL102, 1, l:true)
+" @vimlint(EVL102, 1, l:false)
+function! s:decode(json, ...) abort
+ let settings = extend({
+ \ 'use_token': 0,
+ \}, get(a:000, 0, {}))
+ let json = iconv(a:json, "utf-8", &encoding)
+ let json = join(split(json, "\n"), '')
+ let json = substitute(json, '\\u34;', '\\"', 'g')
+ let json = substitute(json, '\\u\(\x\x\x\x\)', '\=s:string.nr2enc_char("0x".submatch(1))', 'g')
+ if settings.use_token
+ let prefix = '__Web.JSON__'
+ while stridx(json, prefix) != -1
+ let prefix .= '_'
+ endwhile
+ let [null,true,false] = map(['null','true','false'], 'prefix . v:val')
+ sandbox return s:_resolve(eval(json), prefix)
+ else
+ let [null,true,false] = [s:const.null(),s:const.true(),s:const.false()]
+ sandbox return eval(json)
+ endif
+endfunction
+" @vimlint(EVL102, 0, l:null)
+" @vimlint(EVL102, 0, l:true)
+" @vimlint(EVL102, 0, l:false)
+
+function! s:encode(val) abort
+ if type(a:val) == 0
+ return a:val
+ elseif type(a:val) == 1
+ let json = '"' . escape(a:val, '\"') . '"'
+ let json = substitute(json, "\r", '\\r', 'g')
+ let json = substitute(json, "\n", '\\n', 'g')
+ let json = substitute(json, "\t", '\\t', 'g')
+ return iconv(json, &encoding, "utf-8")
+ elseif type(a:val) == 2
+ if s:const.true == a:val
+ return 'true'
+ elseif s:const.false == a:val
+ return 'false'
+ elseif s:const.null == a:val
+ return 'null'
+ else
+ " backward compatibility
+ return string(a:val)
+ endif
+ elseif type(a:val) == 3
+ return '[' . join(map(copy(a:val), 's:encode(v:val)'), ',') . ']'
+ elseif type(a:val) == 4
+ return '{' . join(map(keys(a:val), 's:encode(v:val).":".s:encode(a:val[v:val])'), ',') . '}'
+ else
+ return string(a:val)
+ endif
+endfunction
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+" vim:set et ts=2 sts=2 sw=2 tw=0:
+
+endif
diff --git a/build b/build
index 3865927a..4a7a813c 100755
--- a/build
+++ b/build
@@ -105,6 +105,7 @@ PACKS="
clojure:guns/vim-clojure-static
coffee-script:kchmck/vim-coffee-script
cryptol:victoredwardocallaghan/cryptol.vim
+ crystal:rhysd/vim-crystal
cql:elubow/cql-vim
css:JulesWang/css.vim
cucumber:tpope/vim-cucumber
diff --git a/ftdetect/polyglot.vim b/ftdetect/polyglot.vim
index da43539c..edb46195 100644
--- a/ftdetect/polyglot.vim
+++ b/ftdetect/polyglot.vim
@@ -60,6 +60,12 @@ au! BufRead,BufNewFile *.cyl set filetype=cryptol
au! BufRead,BufNewFile *.lcry set filetype=cryptol
au! BufRead,BufNewFile *.lcyl set filetype=cryptol
endif
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+autocmd BufNewFile,BufReadPost *.cr setlocal filetype=crystal
+autocmd BufNewFile,BufReadPost Projectfile setlocal filetype=crystal
+autocmd BufNewFile,BufReadPost *.ecr setlocal filetype=eruby
+endif
if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'cucumber') == -1
autocmd BufNewFile,BufReadPost *.feature,*.story set filetype=cucumber
diff --git a/ftplugin/crystal.vim b/ftplugin/crystal.vim
new file mode 100644
index 00000000..f990a8da
--- /dev/null
+++ b/ftplugin/crystal.vim
@@ -0,0 +1,60 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+if exists('b:did_ftplugin')
+ finish
+endif
+let b:did_ftplugin = 1
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+if exists('loaded_matchit') && !exists('b:match_words')
+ let b:match_ignorecase = 0
+
+ let b:match_words =
+ \ '\<\%(if\|unless\|case\|while\|until\|for\|do\|class\|module\|struct\|lib\|macro\|ifdef\|def\|fun\|begin\)\>=\@!' .
+ \ ':' .
+ \ '\<\%(else\|elsif\|ensure\|when\|rescue\|break\|redo\|next\|retry\)\>' .
+ \ ':' .
+ \ '\<end\>' .
+ \ ',{:},\[:\],(:)'
+
+ let b:match_skip =
+ \ "synIDattr(synID(line('.'),col('.'),0),'name') =~ '" .
+ \ "\\<crystal\\%(String\\|StringDelimiter\\|ASCIICode\\|Escape\\|" .
+ \ "Interpolation\\|NoInterpolation\\|Comment\\|Documentation\\|" .
+ \ "ConditionalModifier\\|RepeatModifier\\|OptionalDo\\|" .
+ \ "Function\\|BlockArgument\\|KeywordAsMethod\\|ClassVariable\\|" .
+ \ "InstanceVariable\\|GlobalVariable\\|Symbol\\)\\>'"
+endif
+
+setlocal comments=:#
+setlocal commentstring=#\ %s
+setlocal suffixesadd=.cr
+
+" Set format for quickfix window
+setlocal errorformat=
+ \%ESyntax\ error\ in\ line\ %l:\ %m,
+ \%ESyntax\ error\ in\ %f:%l:\ %m,
+ \%EError\ in\ %f:%l:\ %m,
+ \%C%p^,
+ \%-C%.%#
+
+if get(g:, 'crystal_define_mappings', 1)
+ nmap <buffer>gd <Plug>(crystal-jump-to-definition)
+ nmap <buffer>gc <Plug>(crystal-show-context)
+ nmap <buffer>gss <Plug>(crystal-spec-switch)
+ nmap <buffer>gsa <Plug>(crystal-spec-run-all)
+ nmap <buffer>gsc <Plug>(crystal-spec-run-current)
+endif
+
+if &l:ofu ==# ''
+ setlocal omnifunc=crystal_lang#complete
+endif
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+" vim: nowrap sw=2 sts=2 ts=8:
+
+endif
diff --git a/indent/crystal.vim b/indent/crystal.vim
new file mode 100644
index 00000000..fad2822c
--- /dev/null
+++ b/indent/crystal.vim
@@ -0,0 +1,639 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+" Only load this indent file when no other was loaded.
+if exists('b:did_indent')
+ finish
+endif
+let b:did_indent = 1
+
+if !exists('g:crystal_indent_access_modifier_style')
+ " Possible values: "normal", "indent", "outdent"
+ let g:crystal_indent_access_modifier_style = 'normal'
+endif
+
+setlocal nosmartindent
+
+" Now, set up our indentation expression and keys that trigger it.
+setlocal indentexpr=GetCrystalIndent(v:lnum)
+setlocal indentkeys=0{,0},0),0],!^F,o,O,e,:,.
+setlocal indentkeys+==end,=else,=elsif,=when,=ensure,=rescue,==begin,==end
+setlocal indentkeys+==private,=protected,=public
+
+" Only define the function once.
+if exists('*GetCrystalIndent')
+ finish
+endif
+
+let s:cpo_save = &cpo
+set cpo&vim
+
+" 1. Variables {{{1
+" ============
+
+" Regex of syntax group names that are or delimit strings/symbols or are comments.
+let s:syng_strcom = '\<crystal\%(Regexp\|RegexpDelimiter\|RegexpEscape' .
+ \ '\|Symbol\|String\|StringDelimiter\|StringEscape\|ASCIICode' .
+ \ '\|Interpolation\|InterpolationDelimiter\|NoInterpolation\|Comment\|Documentation\)\>'
+
+" Regex of syntax group names that are strings.
+let s:syng_string =
+ \ '\<crystal\%(String\|Interpolation\|NoInterpolation\|StringEscape\)\>'
+
+" Regex of syntax group names that are strings or documentation.
+let s:syng_stringdoc =
+ \'\<crystal\%(String\|Interpolation\|NoInterpolation\|StringEscape\|Documentation\)\>'
+
+" Expression used to check whether we should skip a match with searchpair().
+let s:skip_expr =
+ \ "synIDattr(synID(line('.'),col('.'),1),'name') =~ '".s:syng_strcom."'"
+
+" Regex used for words that, at the start of a line, add a level of indent.
+let s:crystal_indent_keywords =
+ \ '^\s*\zs\<\%(module\|\%(abstract\)\=\s*\%(class\|struct\)\|enum\|if\|for\|macro' .
+ \ '\|while\|until\|else\|elsif\|case\|when\|unless\|begin\|ensure\|rescue\|lib' .
+ \ '\|\%(protected\|private\)\=\s*def\):\@!\>' .
+ \ '\|\%([=,*/%+-]\|<<\|>>\|:\s\)\s*\zs' .
+ \ '\<\%(if\|for\|while\|until\|case\|unless\|begin\):\@!\>' .
+ \ '\|{%\s*\<\%(if\|for\|while\|until\|lib\|case\|unless\|begin\|else\|elsif\|when\)'
+
+" Regex used for words that, at the start of a line, remove a level of indent.
+let s:crystal_deindent_keywords =
+ \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|end\):\@!\>' .
+ \ '\|{%\s*\<\%(ensure\|else\|rescue\|elsif\|when\|end\)\>'
+
+" Regex that defines the start-match for the 'end' keyword.
+" TODO: the do here should be restricted somewhat (only at end of line)?
+let s:end_start_regex =
+ \ '{%\s*\<\%(if\|for\|while\|until\|unless\|begin\|lib\)\>\|' .
+ \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
+ \ '\<\%(module\|\%(abstract\)\=\s*\%(class\|struct\)\|enum\|macro\|if\|for\|while\|until\|case\|unless\|begin\|lib' .
+ \ '\|\%(protected\|private\)\=\s*def\):\@!\>' .
+ \ '\|\%(^\|[^.:@$]\)\@<=\<do:\@!\>'
+
+" Regex that defines the middle-match for the 'end' keyword.
+let s:end_middle_regex =
+ \ '{%\s*\<\%(ensure\|else\|when\|elsif\)\>\s*%}\|' .
+ \ '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue:\@!\>\|when\|elsif\):\@!\>'
+
+" Regex that defines the end-match for the 'end' keyword.
+let s:end_end_regex = '\%(^\|[^.:@$]\)\@<=\<end:\@!\>\|{%\s*\<\%(end\)\>'
+
+" Expression used for searchpair() call for finding match for 'end' keyword.
+let s:end_skip_expr = s:skip_expr .
+ \ ' || (expand("<cword>") == "do"' .
+ \ ' && getline(".") =~ "^\\s*\\<\\(while\\|until\\|for\\):\\@!\\>")'
+
+" Regex that defines continuation lines, not including (, {, or [.
+let s:non_bracket_continuation_regex = '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$'
+
+" Regex that defines continuation lines.
+let s:continuation_regex =
+ \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$'
+
+" Regex that defines continuable keywords
+let s:continuable_regex =
+ \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
+ \ '\<\%(if\|for\|while\|until\|unless\):\@!\>'
+
+" Regex that defines bracket continuations
+let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$'
+
+" Regex that defines end of bracket continuation followed by another continuation
+let s:bracket_switch_continuation_regex = '^\([^(]\+\zs).\+\)\+'.s:continuation_regex
+
+" Regex that defines the first part of a splat pattern
+let s:splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$'
+
+" Regex that defines blocks.
+"
+" Note that there's a slight problem with this regex and s:continuation_regex.
+" Code like this will be matched by both:
+"
+" method_call do |(a, b)|
+"
+" The reason is that the pipe matches a hanging "|" operator.
+"
+let s:block_regex =
+ \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|\s*(*\s*\%([*@&]\=\h\w*,\=\s*\)\%(,\s*(*\s*[*@&]\=\h\w*\s*)*\s*\)*|\)\=\s*\%(#.*\)\=$'
+
+let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex
+
+" Regex that describes a leading operator (only a method call's dot for now)
+let s:leading_operator_regex = '^\s*[.]'
+
+" Regex that describes all indent access modifiers
+let s:access_modifier_regex = '\C^\s*\%(public\|protected\|private\)\s*\%(#.*\)\=$'
+
+" 2. Auxiliary Functions {{{1
+" ======================
+
+" Check if the character at lnum:col is inside a string, comment, or is ascii.
+function s:IsInStringOrComment(lnum, col)
+ return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_strcom
+endfunction
+
+" Check if the character at lnum:col is inside a string.
+function s:IsInString(lnum, col)
+ return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_string
+endfunction
+
+" Check if the character at lnum:col is inside a string or documentation.
+function s:IsInStringOrDocumentation(lnum, col)
+ return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_stringdoc
+endfunction
+
+" Check if the character at lnum:col is inside a string delimiter
+function s:IsInStringDelimiter(lnum, col)
+ return synIDattr(synID(a:lnum, a:col, 1), 'name') ==# 'crystalStringDelimiter'
+endfunction
+
+" Find line above 'lnum' that isn't empty, in a comment, or in a string.
+function s:PrevNonBlankNonString(lnum)
+ let in_block = 0
+ let lnum = prevnonblank(a:lnum)
+ while lnum > 0
+ " Go in and out of blocks comments as necessary.
+ " If the line isn't empty (with opt. comment) or in a string, end search.
+ let line = getline(lnum)
+ if line =~# '^=begin'
+ if in_block
+ let in_block = 0
+ else
+ break
+ endif
+ elseif !in_block && line =~# '^=end'
+ let in_block = 1
+ elseif !in_block && line !~# '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1)
+ \ && s:IsInStringOrComment(lnum, strlen(line)))
+ break
+ endif
+ let lnum = prevnonblank(lnum - 1)
+ endwhile
+ return lnum
+endfunction
+
+" Find line above 'lnum' that started the continuation 'lnum' may be part of.
+function s:GetMSL(lnum)
+ " Start on the line we're at and use its indent.
+ let msl = a:lnum
+ let msl_body = getline(msl)
+ let lnum = s:PrevNonBlankNonString(a:lnum - 1)
+ while lnum > 0
+ " If we have a continuation line, or we're in a string, use line as MSL.
+ " Otherwise, terminate search as we have found our MSL already.
+ let line = getline(lnum)
+
+ if s:Match(msl, s:leading_operator_regex)
+ " If the current line starts with a leading operator, keep its indent
+ " and keep looking for an MSL.
+ let msl = lnum
+ elseif s:Match(lnum, s:splat_regex)
+ " If the above line looks like the "*" of a splat, use the current one's
+ " indentation.
+ "
+ " Example:
+ " Hash[*
+ " method_call do
+ " something
+ "
+ return msl
+ elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
+ \ s:Match(msl, s:non_bracket_continuation_regex)
+ " If the current line is a non-bracket continuation and so is the
+ " previous one, keep its indent and continue looking for an MSL.
+ "
+ " Example:
+ " method_call one,
+ " two,
+ " three
+ "
+ let msl = lnum
+ elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
+ \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
+ " If the current line is a bracket continuation or a block-starter, but
+ " the previous is a non-bracket one, respect the previous' indentation,
+ " and stop here.
+ "
+ " Example:
+ " method_call one,
+ " two {
+ " three
+ "
+ return lnum
+ elseif s:Match(lnum, s:bracket_continuation_regex) &&
+ \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
+ " If both lines are bracket continuations (the current may also be a
+ " block-starter), use the current one's and stop here
+ "
+ " Example:
+ " method_call(
+ " other_method_call(
+ " foo
+ return msl
+ elseif s:Match(lnum, s:block_regex) &&
+ \ !s:Match(msl, s:continuation_regex) &&
+ \ !s:Match(msl, s:block_continuation_regex)
+ " If the previous line is a block-starter and the current one is
+ " mostly ordinary, use the current one as the MSL.
+ "
+ " Example:
+ " method_call do
+ " something
+ " something_else
+ return msl
+ else
+ let col = match(line, s:continuation_regex) + 1
+ if (col > 0 && !s:IsInStringOrComment(lnum, col))
+ \ || s:IsInString(lnum, strlen(line))
+ let msl = lnum
+ else
+ break
+ endif
+ endif
+
+ let msl_body = getline(msl)
+ let lnum = s:PrevNonBlankNonString(lnum - 1)
+ endwhile
+ return msl
+endfunction
+
+" Check if line 'lnum' has more opening brackets than closing ones.
+function s:ExtraBrackets(lnum)
+ let opening = {'parentheses': [], 'braces': [], 'brackets': []}
+ let closing = {'parentheses': [], 'braces': [], 'brackets': []}
+
+ let line = getline(a:lnum)
+ let pos = match(line, '[][(){}]', 0)
+
+ " Save any encountered opening brackets, and remove them once a matching
+ " closing one has been found. If a closing bracket shows up that doesn't
+ " close anything, save it for later.
+ while pos != -1
+ if !s:IsInStringOrComment(a:lnum, pos + 1)
+ if line[pos] ==# '('
+ call add(opening.parentheses, {'type': '(', 'pos': pos})
+ elseif line[pos] ==# ')'
+ if empty(opening.parentheses)
+ call add(closing.parentheses, {'type': ')', 'pos': pos})
+ else
+ let opening.parentheses = opening.parentheses[0:-2]
+ endif
+ elseif line[pos] ==# '{'
+ call add(opening.braces, {'type': '{', 'pos': pos})
+ elseif line[pos] ==# '}'
+ if empty(opening.braces)
+ call add(closing.braces, {'type': '}', 'pos': pos})
+ else
+ let opening.braces = opening.braces[0:-2]
+ endif
+ elseif line[pos] ==# '['
+ call add(opening.brackets, {'type': '[', 'pos': pos})
+ elseif line[pos] ==# ']'
+ if empty(opening.brackets)
+ call add(closing.brackets, {'type': ']', 'pos': pos})
+ else
+ let opening.brackets = opening.brackets[0:-2]
+ endif
+ endif
+ endif
+
+ let pos = match(line, '[][(){}]', pos + 1)
+ endwhile
+
+ " Find the rightmost brackets, since they're the ones that are important in
+ " both opening and closing cases
+ let rightmost_opening = {'type': '(', 'pos': -1}
+ let rightmost_closing = {'type': ')', 'pos': -1}
+
+ for opening in opening.parentheses + opening.braces + opening.brackets
+ if opening.pos > rightmost_opening.pos
+ let rightmost_opening = opening
+ endif
+ endfor
+
+ for closing in closing.parentheses + closing.braces + closing.brackets
+ if closing.pos > rightmost_closing.pos
+ let rightmost_closing = closing
+ endif
+ endfor
+
+ return [rightmost_opening, rightmost_closing]
+endfunction
+
+function s:Match(lnum, regex)
+ let line = getline(a:lnum)
+ let offset = match(line, '\C'.a:regex)
+ let col = offset + 1
+
+ while offset > -1 && s:IsInStringOrComment(a:lnum, col)
+ let offset = match(line, '\C'.a:regex, offset + 1)
+ let col = offset + 1
+ endwhile
+
+ if offset > -1
+ return col
+ else
+ return 0
+ endif
+endfunction
+
+" Locates the containing class/module's definition line, ignoring nested classes
+" along the way.
+"
+function! s:FindContainingClass()
+ let saved_position = getpos('.')
+
+ while searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
+ \ s:end_skip_expr) > 0
+ if expand('<cword>') =~# '\<class\|module\>'
+ let found_lnum = line('.')
+ call setpos('.', saved_position)
+ return found_lnum
+ endif
+ endwhile
+
+ call setpos('.', saved_position)
+ return 0
+endfunction
+
+" 3. GetCrystalIndent Function {{{1
+" =========================
+
+function GetCrystalIndent(...)
+ " 3.1. Setup {{{2
+ " ----------
+
+ " The value of a single shift-width
+ if exists('*shiftwidth')
+ let sw = shiftwidth()
+ else
+ let sw = &sw
+ endif
+
+ " For the current line, use the first argument if given, else v:lnum
+ let clnum = a:0 ? a:1 : v:lnum
+
+ " Set up variables for restoring position in file. Could use clnum here.
+ let vcol = col('.')
+
+ " 3.2. Work on the current line {{{2
+ " -----------------------------
+
+ " Get the current line.
+ let line = getline(clnum)
+ let ind = -1
+
+ " If this line is an access modifier keyword, align according to the closest
+ " class declaration.
+ if g:crystal_indent_access_modifier_style ==? 'indent'
+ if s:Match(clnum, s:access_modifier_regex)
+ let class_line = s:FindContainingClass()
+ if class_line > 0
+ return indent(class_line) + sw
+ endif
+ endif
+ elseif g:crystal_indent_access_modifier_style ==? 'outdent'
+ if s:Match(clnum, s:access_modifier_regex)
+ let class_line = s:FindContainingClass()
+ if class_line > 0
+ return indent(class_line)
+ endif
+ endif
+ endif
+
+ " If we got a closing bracket on an empty line, find its match and indent
+ " according to it. For parentheses we indent to its column - 1, for the
+ " others we indent to the containing line's MSL's level. Return -1 if fail.
+ let col = matchend(line, '^\s*[]})]')
+ if col > 0 && !s:IsInStringOrComment(clnum, col)
+ call cursor(clnum, col)
+ let bs = strpart('(){}[]', stridx(')}]', line[col - 1]) * 2, 2)
+ if searchpair(escape(bs[0], '\['), '', bs[1], 'bW', s:skip_expr) > 0
+ if line[col-1] ==# ')' && col('.') != col('$') - 1
+ let ind = virtcol('.') - 1
+ else
+ let ind = indent(s:GetMSL(line('.')))
+ endif
+ endif
+ return ind
+ endif
+
+ " If we have a =begin or =end set indent to first column.
+ if match(line, '^\s*\%(=begin\|=end\)$') != -1
+ return 0
+ endif
+
+ " If we have a deindenting keyword, find its match and indent to its level.
+ " TODO: this is messy
+ if s:Match(clnum, s:crystal_deindent_keywords)
+ call cursor(clnum, 1)
+ if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
+ \ s:end_skip_expr) > 0
+ let msl = s:GetMSL(line('.'))
+ let line = getline(line('.'))
+
+ if strpart(line, 0, col('.') - 1) =~# '=\s*$' &&
+ \ strpart(line, col('.') - 1, 2) !~# 'do'
+ " assignment to case/begin/etc, on the same line, hanging indent
+ let ind = virtcol('.') - 1
+ elseif getline(msl) =~# '=\s*\(#.*\)\=$'
+ " in the case of assignment to the msl, align to the starting line,
+ " not to the msl
+ let ind = indent(line('.'))
+ else
+ " align to the msl
+ let ind = indent(msl)
+ endif
+ endif
+ return ind
+ endif
+
+ " If we are in a multi-line string or line-comment, don't do anything to it.
+ if s:IsInStringOrDocumentation(clnum, matchend(line, '^\s*') + 1)
+ return indent('.')
+ endif
+
+ " If we are at the closing delimiter of a "<<" heredoc-style string, set the
+ " indent to 0.
+ if line =~# '^\k\+\s*$'
+ \ && s:IsInStringDelimiter(clnum, 1)
+ \ && search('\V<<'.line, 'nbW') > 0
+ return 0
+ endif
+
+ " If the current line starts with a leading operator, add a level of indent.
+ if s:Match(clnum, s:leading_operator_regex)
+ return indent(s:GetMSL(clnum)) + sw
+ endif
+
+ " 3.3. Work on the previous line. {{{2
+ " -------------------------------
+
+ " Find a non-blank, non-multi-line string line above the current line.
+ let lnum = s:PrevNonBlankNonString(clnum - 1)
+
+ " If the line is empty and inside a string, use the previous line.
+ if line =~# '^\s*$' && lnum != prevnonblank(clnum - 1)
+ return indent(prevnonblank(clnum))
+ endif
+
+ " At the start of the file use zero indent.
+ if lnum == 0
+ return 0
+ endif
+
+ " Set up variables for the previous line.
+ let line = getline(lnum)
+ let ind = indent(lnum)
+
+ if s:Match(lnum, s:continuable_regex) && s:Match(lnum, s:continuation_regex)
+ return indent(s:GetMSL(lnum)) + sw + sw
+ endif
+
+ " If the previous line ended with a block opening, add a level of indent.
+ if s:Match(lnum, s:block_regex)
+ let msl = s:GetMSL(lnum)
+
+ if getline(msl) =~# '=\s*\(#.*\)\=$'
+ " in the case of assignment to the msl, align to the starting line,
+ " not to the msl
+ let ind = indent(lnum) + sw
+ else
+ let ind = indent(msl) + sw
+ endif
+ return ind
+ endif
+
+ " If the previous line started with a leading operator, use its MSL's level
+ " of indent
+ if s:Match(lnum, s:leading_operator_regex)
+ return indent(s:GetMSL(lnum))
+ endif
+
+ " If the previous line ended with the "*" of a splat, add a level of indent
+ if line =~ s:splat_regex
+ return indent(lnum) + sw
+ endif
+
+ " If the previous line contained unclosed opening brackets and we are still
+ " in them, find the rightmost one and add indent depending on the bracket
+ " type.
+ "
+ " If it contained hanging closing brackets, find the rightmost one, find its
+ " match and indent according to that.
+ if line =~# '[[({]' || line =~# '[])}]\s*\%(#.*\)\=$'
+ let [opening, closing] = s:ExtraBrackets(lnum)
+
+ if opening.pos != -1
+ if opening.type ==# '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
+ if col('.') + 1 == col('$')
+ return ind + sw
+ else
+ return virtcol('.')
+ endif
+ else
+ let nonspace = matchend(line, '\S', opening.pos + 1) - 1
+ return nonspace > 0 ? nonspace : ind + sw
+ endif
+ elseif closing.pos != -1
+ call cursor(lnum, closing.pos + 1)
+ normal! %
+
+ if s:Match(line('.'), s:crystal_indent_keywords)
+ return indent('.') + sw
+ else
+ return indent('.')
+ endif
+ else
+ call cursor(clnum, vcol)
+ end
+ endif
+
+ " If the previous line ended with an "end", match that "end"s beginning's
+ " indent.
+ let col = s:Match(lnum, '\%(^\|[^.:@$]\)\<end\>\s*\%(#.*\)\=$')
+ if col > 0
+ call cursor(lnum, col)
+ if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW',
+ \ s:end_skip_expr) > 0
+ let n = line('.')
+ let ind = indent('.')
+ let msl = s:GetMSL(n)
+ if msl != n
+ let ind = indent(msl)
+ end
+ return ind
+ endif
+ end
+
+ let col = s:Match(lnum, s:crystal_indent_keywords)
+ if col > 0
+ call cursor(lnum, col)
+ let ind = virtcol('.') - 1 + sw
+ " TODO: make this better (we need to count them) (or, if a searchpair
+ " fails, we know that something is lacking an end and thus we indent a
+ " level
+ if s:Match(lnum, s:end_end_regex)
+ let ind = indent('.')
+ endif
+ return ind
+ endif
+
+ " 3.4. Work on the MSL line. {{{2
+ " --------------------------
+
+ " Set up variables to use and search for MSL to the previous line.
+ let p_lnum = lnum
+ let lnum = s:GetMSL(lnum)
+
+ " If the previous line wasn't a MSL.
+ if p_lnum != lnum
+ " If previous line ends bracket and begins non-bracket continuation decrease indent by 1.
+ if s:Match(p_lnum, s:bracket_switch_continuation_regex)
+ return ind - 1
+ " If previous line is a continuation return its indent.
+ " TODO: the || s:IsInString() thing worries me a bit.
+ elseif s:Match(p_lnum, s:non_bracket_continuation_regex) || s:IsInString(p_lnum,strlen(line))
+ return ind
+ endif
+ endif
+
+ " Set up more variables, now that we know we wasn't continuation bound.
+ let line = getline(lnum)
+ let msl_ind = indent(lnum)
+
+ " If the MSL line had an indenting keyword in it, add a level of indent.
+ " TODO: this does not take into account contrived things such as
+ " module Foo; class Bar; end
+ if s:Match(lnum, s:crystal_indent_keywords)
+ let ind = msl_ind + sw
+ if s:Match(lnum, s:end_end_regex)
+ let ind = ind - sw
+ endif
+ return ind
+ endif
+
+ " If the previous line ended with [*+/.,-=], but wasn't a block ending or a
+ " closing bracket, indent one extra level.
+ if s:Match(lnum, s:non_bracket_continuation_regex) && !s:Match(lnum, '^\s*\([\])}]\|end\)')
+ if lnum == p_lnum
+ let ind = msl_ind + sw
+ else
+ let ind = msl_ind
+ endif
+ return ind
+ endif
+
+ " }}}2
+
+ return ind
+endfunction
+
+" }}}1
+
+let &cpo = s:cpo_save
+unlet s:cpo_save
+
+" vim:set sw=2 sts=2 ts=8 et:
+
+endif
diff --git a/syntax/crystal.vim b/syntax/crystal.vim
new file mode 100644
index 00000000..c361ad70
--- /dev/null
+++ b/syntax/crystal.vim
@@ -0,0 +1,393 @@
+if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1
+
+" Language: Crystal
+" Based on Ruby syntax highlight
+" which is made by Mirko Nasato and Doug Kearns
+" ---------------------------------------------
+
+if exists('b:current_syntax')
+ finish
+endif
+
+syn cluster crystalNotTop contains=@crystalExtendedStringSpecial,@crystalRegexpSpecial,@crystalDeclaration,crystalConditional,crystalExceptional,crystalMethodExceptional,crystalTodo,crystalLinkAttr
+
+if exists('crystal_space_errors')
+ if !exists('crystal_no_trail_space_error')
+ syn match crystalSpaceError display excludenl "\s\+$"
+ endif
+ if !exists('crystal_no_tab_space_error')
+ syn match crystalSpaceError display " \+\t"me=e-1
+ endif
+endif
+
+" Operators
+if exists('crystal_operators')
+ syn match crystalOperator "[~!^&|*/%+-]\|\%(class\s*\)\@<!<<\|<=>\|<=\|\%(<\|\<class\s\+\u\w*\s*\)\@<!<[^<]\@=\|===\|==\|=\~\|>>\|>=\|=\@<!>\|\*\*\|\.\.\.\|\.\.\|::"
+ syn match crystalOperator "->\|-=\|/=\|\*\*=\|\*=\|&&=\|&=\|&&\|||=\||=\|||\|%=\|+=\|!\~\|!="
+ syn region crystalBracketOperator matchgroup=crystalOperator start="\%(\w[?!]\=\|[]})]\)\@<=\[\s*" end="\s*]" contains=ALLBUT,@crystalNotTop
+endif
+
+" Expression Substitution and Backslash Notation
+syn match crystalStringEscape "\\\\\|\\[abefnrstv]\|\\\o\{1,3}\|\\x\x\{1,2}" contained display
+syn match crystalStringEscape "\%(\\M-\\C-\|\\C-\\M-\|\\M-\\c\|\\c\\M-\|\\c\|\\C-\|\\M-\)\%(\\\o\{1,3}\|\\x\x\{1,2}\|\\\=\S\)" contained display
+
+syn region crystalInterpolation matchgroup=crystalInterpolationDelimiter start="#{" end="}" contained contains=ALLBUT,@crystalNotTop
+syn match crystalInterpolation "#\%(\$\|@@\=\)\w\+" display contained contains=crystalInterpolationDelimiter,crystalInstanceVariable,crystalClassVariable,crystalGlobalVariable,crystalPredefinedVariable
+syn match crystalInterpolationDelimiter "#\ze\%(\$\|@@\=\)\w\+" display contained
+syn match crystalInterpolation "#\$\%(-\w\|\W\)" display contained contains=crystalInterpolationDelimiter,crystalPredefinedVariable,crystalInvalidVariable
+syn match crystalInterpolationDelimiter "#\ze\$\%(-\w\|\W\)" display contained
+syn region crystalNoInterpolation start="\\#{" end="}" contained
+syn match crystalNoInterpolation "\\#{" display contained
+syn match crystalNoInterpolation "\\#\%(\$\|@@\=\)\w\+" display contained
+syn match crystalNoInterpolation "\\#\$\W" display contained
+
+syn match crystalDelimEscape "\\[(<{\[)>}\]]" transparent display contained contains=NONE
+
+syn region crystalNestedParentheses start="(" skip="\\\\\|\\)" matchgroup=crystalString end=")" transparent contained
+syn region crystalNestedCurlyBraces start="{" skip="\\\\\|\\}" matchgroup=crystalString end="}" transparent contained
+syn region crystalNestedAngleBrackets start="<" skip="\\\\\|\\>" matchgroup=crystalString end=">" transparent contained
+syn region crystalNestedSquareBrackets start="\[" skip="\\\\\|\\\]" matchgroup=crystalString end="\]" transparent contained
+
+" These are mostly Oniguruma ready
+syn region crystalRegexpComment matchgroup=crystalRegexpSpecial start="(?#" skip="\\)" end=")" contained
+syn region crystalRegexpParens matchgroup=crystalRegexpSpecial start="(\(?:\|?<\=[=!]\|?>\|?<[a-z_]\w*>\|?[imx]*-[imx]*:\=\|\%(?#\)\@!\)" skip="\\)" end=")" contained transparent contains=@crystalRegexpSpecial
+syn region crystalRegexpBrackets matchgroup=crystalRegexpCharClass start="\[\^\=" skip="\\\]" end="\]" contained transparent contains=crystalStringEscape,crystalRegexpEscape,crystalRegexpCharClass oneline
+syn match crystalRegexpCharClass "\\[DdHhSsWw]" contained display
+syn match crystalRegexpCharClass "\[:\^\=\%(alnum\|alpha\|ascii\|blank\|cntrl\|digit\|graph\|lower\|print\|punct\|space\|upper\|xdigit\):\]" contained
+syn match crystalRegexpEscape "\\[].*?+^$|\\/(){}[]" contained
+syn match crystalRegexpQuantifier "[*?+][?+]\=" contained display
+syn match crystalRegexpQuantifier "{\d\+\%(,\d*\)\=}?\=" contained display
+syn match crystalRegexpAnchor "[$^]\|\\[ABbGZz]" contained display
+syn match crystalRegexpDot "\." contained display
+syn match crystalRegexpSpecial "|" contained display
+syn match crystalRegexpSpecial "\\[1-9]\d\=\d\@!" contained display
+syn match crystalRegexpSpecial "\\k<\%([a-z_]\w*\|-\=\d\+\)\%([+-]\d\+\)\=>" contained display
+syn match crystalRegexpSpecial "\\k'\%([a-z_]\w*\|-\=\d\+\)\%([+-]\d\+\)\='" contained display
+syn match crystalRegexpSpecial "\\g<\%([a-z_]\w*\|-\=\d\+\)>" contained display
+syn match crystalRegexpSpecial "\\g'\%([a-z_]\w*\|-\=\d\+\)'" contained display
+
+syn cluster crystalStringSpecial contains=crystalInterpolation,crystalNoInterpolation,crystalStringEscape
+syn cluster crystalExtendedStringSpecial contains=@crystalStringSpecial,crystalNestedParentheses,crystalNestedCurlyBraces,crystalNestedAngleBrackets,crystalNestedSquareBrackets
+syn cluster crystalRegexpSpecial contains=crystalInterpolation,crystalNoInterpolation,crystalStringEscape,crystalRegexpSpecial,crystalRegexpEscape,crystalRegexpBrackets,crystalRegexpCharClass,crystalRegexpDot,crystalRegexpQuantifier,crystalRegexpAnchor,crystalRegexpParens,crystalRegexpComment
+
+" Numbers and ASCII Codes
+syn match crystalASCIICode "\%(\w\|[]})\"'/]\)\@<!\%(?\%(\\M-\\C-\|\\C-\\M-\|\\M-\\c\|\\c\\M-\|\\c\|\\C-\|\\M-\)\=\%(\\\o\{1,3}\|\\x\x\{1,2}\|\\\=\S\)\)"
+syn match crystalInteger "\%(\%(\w\|[]})\"']\s*\)\@<!-\)\=\<0[xX]\x\+\%(_\x\+\)*\%(_*[ufi]\%(32\|64\)\)\=\>" display
+syn match crystalInteger "\%(\%(\w\|[]})\"']\s*\)\@<!-\)\=\<\%(0[dD]\)\=\%(0\|[1-9]\d*\%(_\d\+\)*\)\%(_\x\+\)*\%(_*[ufi]\%(32\|64\)\)\=\>" display
+syn match crystalInteger "\%(\%(\w\|[]})\"']\s*\)\@<!-\)\=\<0[oO]\=\o\+\%(_\o\+\)*\%(_\x\+\)*\%(_*[ufi]\%(32\|64\)\)\=\>" display
+syn match crystalInteger "\%(\%(\w\|[]})\"']\s*\)\@<!-\)\=\<0[bB][01]\+\%(_[01]\+\)*\%(_\x\+\)*\%(_*[ufi]\%(32\|64\)\)\=\>" display
+syn match crystalFloat "\%(\%(\w\|[]})\"']\s*\)\@<!-\)\=\<\%(0\|[1-9]\d*\%(_\d\+\)*\)\.\d\+\%(_\d\+\)*\%(_*f\%(32\|64\)\)\=\>" display
+syn match crystalFloat "\%(\%(\w\|[]})\"']\s*\)\@<!-\)\=\<\%(0\|[1-9]\d*\%(_\d\+\)*\)\%(\.\d\+\%(_\d\+\)*\)\=\%([eE][-+]\=\d\+\%(_\d\+\)*\)\%(_*f\%(32\|64\)\)\=\>" display
+
+" Identifiers
+syn match crystalLocalVariableOrMethod "\<[_[:lower:]][_[:alnum:]]*[?!=]\=" contains=NONE display transparent
+syn match crystalBlockArgument "&[_[:lower:]][_[:alnum:]]" contains=NONE display transparent
+
+syn match crystalConstant "\%(\%([.@$]\@<!\.\)\@<!\<\|::\)\_s*\zs\u\w*\%(\>\|::\)\@="
+syn match crystalClassVariable "@@\%(\h\|%\|[^\x00-\x7F]\)\%(\w\|%\|[^\x00-\x7F]\)*" display
+syn match crystalInstanceVariable "@\%(\h\|%\|[^\x00-\x7F]\)\%(\w\|%\|[^\x00-\x7F]\)*" display
+syn match crystalGlobalVariable "$\%(\%(\h\|%\|[^\x00-\x7F]\)\%(\w\|%\|[^\x00-\x7F]\)*\|-.\)"
+syn match crystalFreshVariable "\%(\h\|[^\x00-\x7F]\)\@<!%\%(\h\|[^\x00-\x7F]\)\%(\w\|%\|[^\x00-\x7F]\)*" display
+syn match crystalSymbol "[]})\"':]\@<!:\%(\^\|\~\|<<\|<=>\|<=\|<\|===\|[=!]=\|[=!]\~\|!\|>>\|>=\|>\||\|-@\|-\|/\|\[][=?]\|\[]\|\*\*\|\*\|&\|%\|+@\|+\|`\)"
+syn match crystalSymbol "[]})\"':]\@<!:\$\%(-.\|[`~<=>_,;:!?/.'"@$*\&+0]\)"
+syn match crystalSymbol "[]})\"':]\@<!:\%(\$\|@@\=\)\=\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*"
+syn match crystalSymbol "[]})\"':]\@<!:\%(\h\|%\|[^\x00-\x7F]\)\%(\w\|%\|[^\x00-\x7F]\)*\%([?!=]>\@!\)\="
+syn match crystalSymbol "\%([{(,]\_s*\)\@<=\l\w*[!?]\=::\@!"he=e-1
+syn match crystalSymbol "[]})\"':]\@<!\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*[!?]\=:\s\@="he=e-1
+syn match crystalSymbol "\%([{(,]\_s*\)\@<=[[:space:],{]\l\w*[!?]\=::\@!"hs=s+1,he=e-1
+syn match crystalSymbol "[[:space:],{]\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*[!?]\=:\s\@="hs=s+1,he=e-1
+syn region crystalSymbol start="[]})\"':]\@<!:\"" end="\"" skip="\\\\\|\\\"" contains=@crystalStringSpecial fold
+
+syn match crystalBlockParameter "\%(\h\|%\|[^\x00-\x7F]\)\%(\w\|%\|[^\x00-\x7F]\)*" contained
+syn region crystalBlockParameterList start="\%(\%(\<do\>\|{\)\s*\)\@<=|" end="|" oneline display contains=crystalBlockParameter
+
+syn match crystalInvalidVariable "$[^ %A-Za-z_-]"
+syn match crystalPredefinedVariable #$[!$&"'*+,./0:;<=>?@\`~]#
+syn match crystalPredefinedVariable "$\d\+" display
+syn match crystalPredefinedVariable "$_\>" display
+syn match crystalPredefinedVariable "$-[0FIKadilpvw]\>" display
+syn match crystalPredefinedVariable "$\%(deferr\|defout\|stderr\|stdin\|stdout\)\>" display
+syn match crystalPredefinedVariable "$\%(DEBUG\|FILENAME\|KCODE\|LOADED_FEATURES\|LOAD_PATH\|PROGRAM_NAME\|SAFE\|VERBOSE\)\>" display
+syn match crystalPredefinedConstant "\%(\%(\.\@<!\.\)\@<!\|::\)\_s*\zs\%(MatchingData\|ARGF\|ARGV\|ENV\)\>\%(\s*(\)\@!"
+syn match crystalPredefinedConstant "\%(\%(\.\@<!\.\)\@<!\|::\)\_s*\zs\%(DATA\|FALSE\|NIL\)\>\%(\s*(\)\@!"
+syn match crystalPredefinedConstant "\%(\%(\.\@<!\.\)\@<!\|::\)\_s*\zs\%(STDERR\|STDIN\|STDOUT\|TOPLEVEL_BINDING\|TRUE\)\>\%(\s*(\)\@!"
+syn match crystalPredefinedConstant "\%(\%(\.\@<!\.\)\@<!\|::\)\_s*\zs\%(crystal_\%(VERSION\|RELEASE_DATE\|PLATFORM\|PATCHLEVEL\|REVISION\|DESCRIPTION\|COPYRIGHT\|ENGINE\)\)\>\%(\s*(\)\@!"
+
+" Normal Regular Expression
+syn region crystalRegexp matchgroup=crystalRegexpDelimiter start="\%(\%(^\|\<\%(and\|or\|while\|until\|unless\|if\|elsif\|ifdef\|when\|not\|then\|else\)\|[;\~=!|&(,[<>?:*+-]\)\s*\)\@<=/" end="/[iomxneus]*" skip="\\\\\|\\/" contains=@crystalRegexpSpecial fold
+syn region crystalRegexp matchgroup=crystalRegexpDelimiter start="\%(\h\k*\s\+\)\@<=/[ \t=]\@!" end="/[iomxneus]*" skip="\\\\\|\\/" contains=@crystalRegexpSpecial fold
+
+" Generalized Regular Expression
+syn region crystalRegexp matchgroup=crystalRegexpDelimiter start="%r\z([~`!@#$%^&*_\-+=|\:;"',.? /]\)" end="\z1[iomxneus]*" skip="\\\\\|\\\z1" contains=@crystalRegexpSpecial fold
+syn region crystalRegexp matchgroup=crystalRegexpDelimiter start="%r{" end="}[iomxneus]*" skip="\\\\\|\\}" contains=@crystalRegexpSpecial fold
+syn region crystalRegexp matchgroup=crystalRegexpDelimiter start="%r<" end=">[iomxneus]*" skip="\\\\\|\\>" contains=@crystalRegexpSpecial,crystalNestedAngleBrackets,crystalDelimEscape fold
+syn region crystalRegexp matchgroup=crystalRegexpDelimiter start="%r\[" end="\][iomxneus]*" skip="\\\\\|\\\]" contains=@crystalRegexpSpecial fold
+syn region crystalRegexp matchgroup=crystalRegexpDelimiter start="%r(" end=")[iomxneus]*" skip="\\\\\|\\)" contains=@crystalRegexpSpecial fold
+
+" Normal String and Shell Command Output
+syn region crystalString matchgroup=crystalStringDelimiter start="\"" end="\"" skip="\\\\\|\\\"" contains=@crystalStringSpecial,@Spell fold
+syn region crystalString matchgroup=crystalStringDelimiter start="`" end="`" skip="\\\\\|\\`" contains=@crystalStringSpecial fold
+
+" Character
+syn match crystalCharLiteral "'\%([^\\]\|\\[abefnrstv'\\]\|\\\o\{1,3}\|\\x\x\{1,2}\|\\u\x\{4}\)'" contained display
+
+" Generalized Single Quoted String, Symbol and Array of Strings
+syn region crystalString matchgroup=crystalStringDelimiter start="%[qwi]\z([~`!@#$%^&*_\-+=|\:;"',.?/]\)" end="\z1" skip="\\\\\|\\\z1" fold
+syn region crystalString matchgroup=crystalStringDelimiter start="%[qwi]{" end="}" skip="\\\\\|\\}" fold contains=crystalNestedCurlyBraces,crystalDelimEscape
+syn region crystalString matchgroup=crystalStringDelimiter start="%[qwi]<" end=">" skip="\\\\\|\\>" fold contains=crystalNestedAngleBrackets,crystalDelimEscape
+syn region crystalString matchgroup=crystalStringDelimiter start="%[qwi]\[" end="\]" skip="\\\\\|\\\]" fold contains=crystalNestedSquareBrackets,crystalDelimEscape
+syn region crystalString matchgroup=crystalStringDelimiter start="%[qwi](" end=")" skip="\\\\\|\\)" fold contains=crystalNestedParentheses,crystalDelimEscape
+syn region crystalString matchgroup=crystalStringDelimiter start="%q " end=" " skip="\\\\\|\\)" fold
+syn region crystalSymbol matchgroup=crystalSymbolDelimiter start="%s\z([~`!@#$%^&*_\-+=|\:;"',.? /]\)" end="\z1" skip="\\\\\|\\\z1" fold
+syn region crystalSymbol matchgroup=crystalSymbolDelimiter start="%s{" end="}" skip="\\\\\|\\}" fold contains=crystalNestedCurlyBraces,crystalDelimEscape
+syn region crystalSymbol matchgroup=crystalSymbolDelimiter start="%s<" end=">" skip="\\\\\|\\>" fold contains=crystalNestedAngleBrackets,crystalDelimEscape
+syn region crystalSymbol matchgroup=crystalSymbolDelimiter start="%s\[" end="\]" skip="\\\\\|\\\]" fold contains=crystalNestedSquareBrackets,crystalDelimEscape
+syn region crystalSymbol matchgroup=crystalSymbolDelimiter start="%s(" end=")" skip="\\\\\|\\)" fold contains=crystalNestedParentheses,crystalDelimEscape
+
+" Generalized Double Quoted String and Array of Strings and Shell Command Output
+" Note: %= is not matched here as the beginning of a double quoted string
+syn region crystalString matchgroup=crystalStringDelimiter start="%\z([~`!@#$%^&*_\-+|\:;"',.?/]\)" end="\z1" skip="\\\\\|\\\z1" contains=@crystalStringSpecial fold
+syn region crystalString matchgroup=crystalStringDelimiter start="%[QWIx]\z([~`!@#$%^&*_\-+=|\:;"',.?/]\)" end="\z1" skip="\\\\\|\\\z1" contains=@crystalStringSpecial fold
+syn region crystalString matchgroup=crystalStringDelimiter start="%[QWIx]\={" end="}" skip="\\\\\|\\}" contains=@crystalStringSpecial,crystalNestedCurlyBraces,crystalDelimEscape fold
+syn region crystalString matchgroup=crystalStringDelimiter start="%[QWIx]\=<" end=">" skip="\\\\\|\\>" contains=@crystalStringSpecial,crystalNestedAngleBrackets,crystalDelimEscape fold
+syn region crystalString matchgroup=crystalStringDelimiter start="%[QWIx]\=\[" end="\]" skip="\\\\\|\\\]" contains=@crystalStringSpecial,crystalNestedSquareBrackets,crystalDelimEscape fold
+syn region crystalString matchgroup=crystalStringDelimiter start="%[QWIx]\=(" end=")" skip="\\\\\|\\)" contains=@crystalStringSpecial,crystalNestedParentheses,crystalDelimEscape fold
+syn region crystalString matchgroup=crystalStringDelimiter start="%[Qx] " end=" " skip="\\\\\|\\)" contains=@crystalStringSpecial fold
+
+" Here Document
+syn region crystalHeredocStart matchgroup=crystalStringDelimiter start=+\%(\%(class\s*\|\%([]})"'.]\|::\)\)\_s*\|\w\)\@<!<<-\=\zs\%(\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*\)+ end=+$+ oneline contains=ALLBUT,@crystalNotTop
+syn region crystalHeredocStart matchgroup=crystalStringDelimiter start=+\%(\%(class\s*\|\%([]})"'.]\|::\)\)\_s*\|\w\)\@<!<<-\=\zs"\%([^"]*\)"+ end=+$+ oneline contains=ALLBUT,@crystalNotTop
+syn region crystalHeredocStart matchgroup=crystalStringDelimiter start=+\%(\%(class\s*\|\%([]})"'.]\|::\)\)\_s*\|\w\)\@<!<<-\=\zs'\%([^']*\)'+ end=+$+ oneline contains=ALLBUT,@crystalNotTop
+syn region crystalHeredocStart matchgroup=crystalStringDelimiter start=+\%(\%(class\s*\|\%([]})"'.]\|::\)\)\_s*\|\w\)\@<!<<-\=\zs`\%([^`]*\)`+ end=+$+ oneline contains=ALLBUT,@crystalNotTop
+
+syn region crystalString start=+\%(\%(class\|::\)\_s*\|\%([]})"'.]\)\s\|\w\)\@<!<<\z(\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*\)\ze\%(.*<<-\=['`"]\=\h\)\@!+hs=s+2 matchgroup=crystalStringDelimiter end=+^\z1$+ contains=crystalHeredocStart,crystalHeredoc,@crystalStringSpecial fold keepend
+syn region crystalString start=+\%(\%(class\|::\)\_s*\|\%([]})"'.]\)\s\|\w\)\@<!<<"\z([^"]*\)"\ze\%(.*<<-\=['`"]\=\h\)\@!+hs=s+2 matchgroup=crystalStringDelimiter end=+^\z1$+ contains=crystalHeredocStart,crystalHeredoc,@crystalStringSpecial fold keepend
+syn region crystalString start=+\%(\%(class\|::\)\_s*\|\%([]})"'.]\)\s\|\w\)\@<!<<'\z([^']*\)'\ze\%(.*<<-\=['`"]\=\h\)\@!+hs=s+2 matchgroup=crystalStringDelimiter end=+^\z1$+ contains=crystalHeredocStart,crystalHeredoc fold keepend
+syn region crystalString start=+\%(\%(class\|::\)\_s*\|\%([]})"'.]\)\s\|\w\)\@<!<<`\z([^`]*\)`\ze\%(.*<<-\=['`"]\=\h\)\@!+hs=s+2 matchgroup=crystalStringDelimiter end=+^\z1$+ contains=crystalHeredocStart,crystalHeredoc,@crystalStringSpecial fold keepend
+
+syn region crystalString start=+\%(\%(class\|::\)\_s*\|\%([]}).]\)\s\|\w\)\@<!<<-\z(\%(\h\|[^\x00-\x7F]\)\%(\w\|[^\x00-\x7F]\)*\)\ze\%(.*<<-\=['`"]\=\h\)\@!+hs=s+3 matchgroup=crystalStringDelimiter end=+^\s*\zs\z1$+ contains=crystalHeredocStart,@crystalStringSpecial fold keepend
+syn region crystalString start=+\%(\%(class\|::\)\_s*\|\%([]}).]\)\s\|\w\)\@<!<<-"\z([^"]*\)"\ze\%(.*<<-\=['`"]\=\h\)\@!+hs=s+3 matchgroup=crystalStringDelimiter end=+^\s*\zs\z1$+ contains=crystalHeredocStart,@crystalStringSpecial fold keepend
+syn region crystalString start=+\%(\%(class\|::\)\_s*\|\%([]}).]\)\s\|\w\)\@<!<<-'\z([^']*\)'\ze\%(.*<<-\=['`"]\=\h\)\@!+hs=s+3 matchgroup=crystalStringDelimiter end=+^\s*\zs\z1$+ contains=crystalHeredocStart fold keepend
+syn region crystalString start=+\%(\%(class\|::\)\_s*\|\%([]}).]\)\s\|\w\)\@<!<<-`\z([^`]*\)`\ze\%(.*<<-\=['`"]\=\h\)\@!+hs=s+3 matchgroup=crystalStringDelimiter end=+^\s*\zs\z1$+ contains=crystalHeredocStart,@crystalStringSpecial fold keepend
+
+if exists('main_syntax') && g:main_syntax ==# 'ecrystal'
+ let b:crystal_no_expensive = 1
+end
+
+syn match crystalAliasDeclaration "[^[:space:];#.()]\+" contained contains=crystalSymbol,crystalGlobalVariable,crystalPredefinedVariable nextgroup=crystalAliasDeclaration2 skipwhite
+syn match crystalAliasDeclaration2 "[^[:space:];#.()]\+" contained contains=crystalSymbol,crystalGlobalVariable,crystalPredefinedVariable
+syn match crystalMethodDeclaration "[^[:space:];#(]\+" contained contains=crystalConstant,crystalBoolean,crystalPseudoVariable,crystalInstanceVariable,crystalClassVariable,crystalGlobalVariable
+syn match crystalFunctionDeclaration "[^[:space:];#=]\+" contained contains=crystalConstant
+syn match crystalTypeDeclaration "[^[:space:];#=]\+" contained contains=crystalConstant
+syn match crystalClassDeclaration "[^[:space:];#<]\+" contained contains=crystalConstant,crystalOperator
+syn match crystalModuleDeclaration "[^[:space:];#<]\+" contained contains=crystalConstant,crystalOperator
+syn match crystalStructDeclaration "[^[:space:];#<]\+" contained contains=crystalConstant,crystalOperator
+syn match crystalLibDeclaration "[^[:space:];#<]\+" contained contains=crystalConstant,crystalOperator
+syn match crystalMacroDeclaration "[^[:space:];#<\"]\+" contained contains=crystalConstant,crystalOperator
+syn match crystalEnumDeclaration "[^[:space:];#<\"]\+" contained contains=crystalConstant
+syn match crystalFunction "\<[_[:alpha:]][_[:alnum:]]*[?!=]\=[[:alnum:]_.:?!=]\@!" contained containedin=crystalMethodDeclaration,crystalFunctionDeclaration
+syn match crystalFunction "\%(\s\|^\)\@<=[_[:alpha:]][_[:alnum:]]*[?!=]\=\%(\s\|$\)\@=" contained containedin=crystalAliasDeclaration,crystalAliasDeclaration2
+syn match crystalFunction "\%([[:space:].]\|^\)\@<=\%(\[\][=?]\=\|\*\*\|[+-]@\=\|[*/%|&^~]\|<<\|>>\|[<>]=\=\|<=>\|===\|[=!]=\|[=!]\~\|!\|`\)\%([[:space:];#(]\|$\)\@=" contained containedin=crystalAliasDeclaration,crystalAliasDeclaration2,crystalMethodDeclaration,crystalFunctionDeclaration
+
+syn cluster crystalDeclaration contains=crystalAliasDeclaration,crystalAliasDeclaration2,crystalMethodDeclaration,crystalFunctionDeclaration,crystalModuleDeclaration,crystalClassDeclaration,crystalStructDeclaration,crystalLibDeclaration,crystalMacroDeclaration,crystalFunction,crystalBlockParameter,crystalTypeDeclaration,crystalEnumDeclaration
+
+" Keywords
+" Note: the following keywords have already been defined:
+" begin case class def do end for if module unless until while
+syn match crystalControl "\<\%(break\|in\|next\|rescue\|return\)\>[?!]\@!"
+syn match crystalOperator "\<defined?" display
+syn match crystalKeyword "\<\%(super\|previous_def\|yield\|as\|of\|with\)\>[?!]\@!"
+syn match crystalBoolean "\<\%(true\|false\)\>[?!]\@!"
+syn match crystalPseudoVariable "\<\%(nil\|self\|__DIR__\|__FILE__\|__LINE__\)\>[?!]\@!" " TODO: reorganise
+
+" Expensive Mode - match 'end' with the appropriate opening keyword for syntax
+" based folding and special highlighting of module/class/method definitions
+if !exists('b:crystal_no_expensive') && !exists('crystal_no_expensive')
+ syn match crystalDefine "\<alias\>" nextgroup=crystalAliasDeclaration skipwhite skipnl
+ syn match crystalDefine "\<def\>" nextgroup=crystalMethodDeclaration skipwhite skipnl
+ syn match crystalDefine "\<fun\>" nextgroup=crystalFunctionDeclaration skipwhite skipnl
+ syn match crystalDefine "\<undef\>" nextgroup=crystalFunction skipwhite skipnl
+ syn match crystalDefine "\<\%(type\|alias\)\>\%(\s*\h\w*\s*=\)\@=" nextgroup=crystalTypeDeclaration skipwhite skipnl
+ syn match crystalClass "\<class\>" nextgroup=crystalClassDeclaration skipwhite skipnl
+ syn match crystalModule "\<module\>" nextgroup=crystalModuleDeclaration skipwhite skipnl
+ syn match crystalStruct "\<struct\>" nextgroup=crystalStructDeclaration skipwhite skipnl
+ syn match crystalLib "\<lib\>" nextgroup=crystalLibDeclaration skipwhite skipnl
+ syn match crystalMacro "\<macro\>" nextgroup=crystalMacroDeclaration skipwhite skipnl
+ syn match crystalEnum "\<enum\>" nextgroup=crystalEnumDeclaration skipwhite skipnl
+
+ syn region crystalMethodBlock start="\<\%(def\|macro\)\>" matchgroup=crystalDefine end="\%(\<\%(def\|macro\)\_s\+\)\@<!\<end\>" contains=ALLBUT,@crystalNotTop fold
+ syn region crystalBlock start="\<class\>" matchgroup=crystalClass end="\<end\>" contains=ALLBUT,@crystalNotTop fold
+ syn region crystalBlock start="\<module\>" matchgroup=crystalModule end="\<end\>" contains=ALLBUT,@crystalNotTop fold
+ syn region crystalBlock start="\<struct\>" matchgroup=crystalStruct end="\<end\>" contains=ALLBUT,@crystalNotTop fold
+ syn region crystalBlock start="\<lib\>" matchgroup=crystalLib end="\<end\>" contains=ALLBUT,@crystalNotTop fold
+ syn region crystalBlock start="\<enum\>" matchgroup=crystalEnum end="\<end\>" contains=ALLBUT,@crystalNotTop fold
+
+ " modifiers
+ syn match crystalConditionalModifier "\<\%(if\|unless\|ifdef\)\>" display
+ syn match crystalRepeatModifier "\<\%(while\|until\)\>" display
+
+ syn region crystalDoBlock matchgroup=crystalControl start="\<do\>" end="\<end\>" contains=ALLBUT,@crystalNotTop fold
+ " curly bracket block or hash literal
+ syn region crystalCurlyBlock matchgroup=crystalCurlyBlockDelimiter start="{" end="}" contains=ALLBUT,@crystalNotTop fold
+ syn region crystalArrayLiteral matchgroup=crystalArrayDelimiter start="\%(\w\|[\]})]\)\@<!\[" end="]" contains=ALLBUT,@crystalNotTop fold
+
+ " statements without 'do'
+ syn region crystalBlockExpression matchgroup=crystalControl start="\<begin\>" end="\<end\>" contains=ALLBUT,@crystalNotTop fold
+ syn region crystalCaseExpression matchgroup=crystalConditional start="\<case\>" end="\<end\>" contains=ALLBUT,@crystalNotTop fold
+ syn region crystalConditionalExpression matchgroup=crystalConditional start="\%(\%(^\|\.\.\.\=\|[{:,;([<>~\*/%&^|+=-]\|\%(\<[_[:lower:]][_[:alnum:]]*\)\@<![?!]\)\s*\)\@<=\%(if\|ifdef\|unless\)\>" end="\%(\%(\%(\.\@<!\.\)\|::\)\s*\)\@<!\<end\>" contains=ALLBUT,@crystalNotTop fold
+
+ syn match crystalConditional "\<\%(then\|else\|when\)\>[?!]\@!" contained containedin=crystalCaseExpression
+ syn match crystalConditional "\<\%(then\|else\|elsif\)\>[?!]\@!" contained containedin=crystalConditionalExpression
+
+ syn match crystalExceptional "\<\%(\%(\%(;\|^\)\s*\)\@<=rescue\|else\|ensure\)\>[?!]\@!" contained containedin=crystalBlockExpression
+ syn match crystalMethodExceptional "\<\%(\%(\%(;\|^\)\s*\)\@<=rescue\|else\|ensure\)\>[?!]\@!" contained containedin=crystalMethodBlock
+
+ " statements with optional 'do'
+ syn region crystalOptionalDoLine matchgroup=crystalRepeat start="\<for\>[?!]\@!" start="\%(\%(^\|\.\.\.\=\|[{:,;([<>~\*/%&^|+-]\|\%(\<[_[:lower:]][_[:alnum:]]*\)\@<![!=?]\)\s*\)\@<=\<\%(until\|while\)\>" matchgroup=crystalOptionalDo end="\%(\<do\>\)" end="\ze\%(;\|$\)" oneline contains=ALLBUT,@crystalNotTop
+ syn region crystalRepeatExpression start="\<for\>[?!]\@!" start="\%(\%(^\|\.\.\.\=\|[{:,;([<>~\*/%&^|+-]\|\%(\<[_[:lower:]][_[:alnum:]]*\)\@<![!=?]\)\s*\)\@<=\<\%(until\|while\)\>" matchgroup=crystalRepeat end="\<end\>" contains=ALLBUT,@crystalNotTop nextgroup=crystalOptionalDoLine fold
+
+ if !exists('crystal_minlines')
+ let crystal_minlines = 500
+ endif
+ exec 'syn sync minlines=' . crystal_minlines
+
+else
+ syn match crystalControl "\<def\>[?!]\@!" nextgroup=crystalMethodDeclaration skipwhite skipnl
+ syn match crystalControl "\<fun\>[?!]\@!" nextgroup=crystalFunctionDeclaration skipwhite skipnl
+ syn match crystalControl "\<class\>[?!]\@!" nextgroup=crystalClassDeclaration skipwhite skipnl
+ syn match crystalControl "\<module\>[?!]\@!" nextgroup=crystalModuleDeclaration skipwhite skipnl
+ syn match crystalControl "\<struct\>[?!]\@!" nextgroup=crystalStructDeclaration skipwhite skipnl
+ syn match crystalControl "\<lib\>[?!]\@!" nextgroup=crystalLibDeclaration skipwhite skipnl
+ syn match crystalControl "\<macro\>[?!]\@!" nextgroup=crystalMacroDeclaration skipwhite skipnl
+ syn match crystalControl "\<enum\>[?!]\@!" nextgroup=crystalEnumDeclaration skipwhite skipnl
+ syn match crystalControl "\<\%(case\|begin\|do\|for\|if\|ifdef\|unless\|while\|until\|else\|elsif\|ensure\|then\|when\|end\)\>[?!]\@!"
+ syn match crystalKeyword "\<\%(alias\|undef\)\>[?!]\@!"
+endif
+
+" Link attribute
+syn region crystalLinkAttrRegion start="@\[" nextgroup=crystalLinkAttrRegionInner end="]" contains=crystalLinkAttr,crystalLinkAttrRegionInner transparent display oneline
+syn region crystalLinkAttrRegionInner start="\%(@\[\)\@<=" end="]\@=" contained contains=ALLBUT,@crystalNotTop transparent display oneline
+syn match crystalLinkAttr "@\[" contained containedin=crystalLinkAttrRegion display
+syn match crystalLinkAttr "]" contained containedin=crystalLinkAttrRegion display
+
+" Special Methods
+if !exists('crystal_no_special_methods')
+ syn keyword crystalAccess protected private
+ " attr is a common variable name
+ syn keyword crystalAttribute getter setter property abstract
+ syn match crystalControl "\<\%(abort\|at_exit\|exit\|fork\|loop\)\>[?!]\@!" display
+ syn keyword crystalException raise
+ " false positive with 'include?'
+ syn match crystalInclude "\<include\>[?!]\@!" display
+ syn keyword crystalInclude extend require
+ syn keyword crystalKeyword caller typeof pointerof sizeof instance_sizeof
+ syn match crystalRecord "\<record\>[?!]\@!" display
+endif
+
+" Macro
+syn region crystalMacroRegion start="{%" end="%}" contains=ALLBUT,@crystalNotTop transparent display oneline
+syn region crystalMacroRegion start="{{" end="}}" contains=ALLBUT,@crystalNotTop transparent display oneline
+syn match crystalMacro "\%({%\|%}\|{{\|}}\)" nextgroup=crystalMacroRegion skipwhite display
+
+" Comments and Documentation
+syn match crystalSharpBang "\%^#!.*" display
+syn keyword crystalTodo FIXME NOTE TODO OPTIMIZE XXX todo contained
+syn match crystalComment "#.*" contains=crystalSharpBang,crystalSpaceError,crystalTodo,@Spell
+if !exists('crystal_no_comment_fold')
+ syn region crystalMultilineComment start="\%(\%(^\s*#.*\n\)\@<!\%(^\s*#.*\n\)\)\%(\(^\s*#.*\n\)\{1,}\)\@=" end="\%(^\s*#.*\n\)\@<=\%(^\s*#.*\n\)\%(^\s*#\)\@!" contains=crystalComment transparent fold keepend
+endif
+
+" Note: this is a hack to prevent 'keywords' being highlighted as such when called as methods with an explicit receiver
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(alias\|begin\|break\|case\|class\|def\|defined\|do\|else\)\>" transparent contains=NONE
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(elsif\|end\|ensure\|false\|for\|if\|ifdef\|in\|module\|next\|nil\)\>" transparent contains=NONE
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(rescue\|return\|self\|super\|previous_def\|then\|true\)\>" transparent contains=NONE
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(undef\|unless\|until\|when\|while\|yield\|with\|__FILE__\|__LINE__\)\>" transparent contains=NONE
+
+syn match crystalKeywordAsMethod "\<\%(alias\|begin\|case\|class\|def\|do\|end\)[?!]" transparent contains=NONE
+syn match crystalKeywordAsMethod "\<\%(if\|ifdef\|module\|undef\|unless\|until\|while\)[?!]" transparent contains=NONE
+
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(abort\|at_exit\|caller\|exit\)\>" transparent contains=NONE
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(extend\|fork\|include\)\>" transparent contains=NONE
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(loop\|private\|protected\)\>" transparent contains=NONE
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(require\|raise\)\>" transparent contains=NONE
+syn match crystalKeywordAsMethod "\%(\%(\.\@<!\.\)\|::\)\_s*\%(typeof\|pointerof\|sizeof\|instance_sizeof\|\)\>" transparent contains=NONE
+
+hi def link crystalClass crystalDefine
+hi def link crystalModule crystalDefine
+hi def link crystalStruct crystalDefine
+hi def link crystalLib crystalDefine
+hi def link crystalMacro crystalDefine
+hi def link crystalEnum crystalDefine
+hi def link crystalMethodExceptional crystalDefine
+hi def link crystalDefine Define
+hi def link crystalFunction Function
+hi def link crystalConditional Conditional
+hi def link crystalConditionalModifier crystalConditional
+hi def link crystalExceptional crystalConditional
+hi def link crystalRepeat Repeat
+hi def link crystalRepeatModifier crystalRepeat
+hi def link crystalOptionalDo crystalRepeat
+hi def link crystalControl Statement
+hi def link crystalInclude Include
+hi def link crystalRecord Statement
+hi def link crystalInteger Number
+hi def link crystalASCIICode Character
+hi def link crystalFloat Float
+hi def link crystalBoolean Boolean
+hi def link crystalException Exception
+if !exists('crystal_no_identifiers')
+ hi def link crystalIdentifier Identifier
+else
+ hi def link crystalIdentifier NONE
+endif
+hi def link crystalClassVariable crystalIdentifier
+hi def link crystalConstant Type
+hi def link crystalGlobalVariable crystalIdentifier
+hi def link crystalBlockParameter crystalIdentifier
+hi def link crystalInstanceVariable crystalIdentifier
+hi def link crystalFreshVariable crystalIdentifier
+hi def link crystalPredefinedIdentifier crystalIdentifier
+hi def link crystalPredefinedConstant crystalPredefinedIdentifier
+hi def link crystalPredefinedVariable crystalPredefinedIdentifier
+hi def link crystalSymbol Constant
+hi def link crystalKeyword Keyword
+hi def link crystalOperator Operator
+hi def link crystalAccess Statement
+hi def link crystalAttribute Statement
+hi def link crystalPseudoVariable Constant
+hi def link crystalCharLiteral Character
+
+hi def link crystalComment Comment
+hi def link crystalTodo Todo
+
+hi def link crystalStringEscape Special
+hi def link crystalInterpolationDelimiter Delimiter
+hi def link crystalNoInterpolation crystalString
+hi def link crystalSharpBang PreProc
+hi def link crystalRegexpDelimiter crystalStringDelimiter
+hi def link crystalSymbolDelimiter crystalStringDelimiter
+hi def link crystalStringDelimiter Delimiter
+hi def link crystalHeredoc crystalString
+hi def link crystalString String
+hi def link crystalRegexpEscape crystalRegexpSpecial
+hi def link crystalRegexpQuantifier crystalRegexpSpecial
+hi def link crystalRegexpAnchor crystalRegexpSpecial
+hi def link crystalRegexpDot crystalRegexpCharClass
+hi def link crystalRegexpCharClass crystalRegexpSpecial
+hi def link crystalRegexpSpecial Special
+hi def link crystalRegexpComment Comment
+hi def link crystalRegexp crystalString
+
+hi def link crystalLinkAttr PreProc
+
+hi def link crystalMacro PreProc
+
+hi def link crystalInvalidVariable Error
+hi def link crystalError Error
+hi def link crystalSpaceError crystalError
+
+let b:current_syntax = 'crystal'
+
+" vim: nowrap sw=2 sts=2 ts=8 noet:
+
+endif