diff options
author | Adam Stankiewicz <sheerun@sher.pl> | 2016-05-02 10:49:45 +0200 |
---|---|---|
committer | Adam Stankiewicz <sheerun@sher.pl> | 2016-05-02 10:49:45 +0200 |
commit | c200e7a0c587f70611b8dd702d0c3b378676a39a (patch) | |
tree | 960c7c88f634854cf3488d6d18ce42344875f8ef /autoload/crystal_lang.vim | |
parent | 5529a5e8e21e4577e4cd3551f2cbad59b5b406e8 (diff) | |
download | vim-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.vim | 332 |
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 |