summaryrefslogtreecommitdiffstats
path: root/autoload/crystal_lang.vim
diff options
context:
space:
mode:
authorAdam Stankiewicz <sheerun@sher.pl>2016-05-02 10:49:45 +0200
committerAdam Stankiewicz <sheerun@sher.pl>2016-05-02 10:49:45 +0200
commitc200e7a0c587f70611b8dd702d0c3b378676a39a (patch)
tree960c7c88f634854cf3488d6d18ce42344875f8ef /autoload/crystal_lang.vim
parent5529a5e8e21e4577e4cd3551f2cbad59b5b406e8 (diff)
downloadvim-polyglot-c200e7a0c587f70611b8dd702d0c3b378676a39a.tar.gz
vim-polyglot-c200e7a0c587f70611b8dd702d0c3b378676a39a.zip
Add crystal syntax, closes #118
Diffstat (limited to 'autoload/crystal_lang.vim')
-rw-r--r--autoload/crystal_lang.vim332
1 files changed, 332 insertions, 0 deletions
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