2019-03-04 03:28:35 -05:00
|
|
|
if exists('g:polyglot_disabled') && index(g:polyglot_disabled, 'crystal') != -1
|
|
|
|
finish
|
|
|
|
endif
|
|
|
|
|
2016-05-02 04:49:45 -04:00
|
|
|
let s:save_cpo = &cpo
|
|
|
|
set cpo&vim
|
|
|
|
|
2018-01-10 17:50:02 -05:00
|
|
|
let s:V = vital#crystal#new()
|
2016-05-02 04:49:45 -04:00
|
|
|
let s:P = s:V.import('Process')
|
|
|
|
let s:C = s:V.import('ColorEcho')
|
|
|
|
|
2018-01-10 17:50:02 -05:00
|
|
|
if exists('*json_decode')
|
|
|
|
function! s:decode_json(text) abort
|
|
|
|
return json_decode(a:text)
|
|
|
|
endfunction
|
|
|
|
else
|
|
|
|
let s:J = s:V.import('Web.JSON')
|
|
|
|
function! s:decode_json(text) abort
|
|
|
|
return s:J.decode(a:text)
|
|
|
|
endfunction
|
|
|
|
endif
|
|
|
|
|
2016-05-02 04:49:45 -04:00
|
|
|
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
|
|
|
|
|
2019-03-29 15:30:36 -04:00
|
|
|
function! s:find_root_by(search_dir, d) abort
|
|
|
|
let found_dir = finddir(a:search_dir, a:d . ';')
|
|
|
|
if found_dir ==# ''
|
2016-05-02 04:49:45 -04:00
|
|
|
return ''
|
|
|
|
endif
|
|
|
|
|
2019-03-29 15:30:36 -04:00
|
|
|
" Note: ':h:h' for {root}/{search_dir}/ -> {root}/{search_dir} -> {root}
|
|
|
|
return fnamemodify(found_dir, ':p:h:h')
|
|
|
|
endfunction
|
|
|
|
|
|
|
|
" Search the root directory containing a 'spec/' and a 'src/' directories.
|
|
|
|
"
|
|
|
|
" Searching for the 'spec/' directory is not enough: for example the crystal
|
|
|
|
" compiler has a 'cr_sources/src/spec/' directory that would otherwise give the root
|
|
|
|
" directory as 'cr_source/src/' instead of 'cr_sources/'.
|
|
|
|
function! s:find_root_by_spec_and_src(d) abort
|
|
|
|
" Search for 'spec/'
|
|
|
|
let root = s:find_root_by('spec', a:d)
|
|
|
|
" Check that 'src/' is also there
|
|
|
|
if root !=# '' && isdirectory(root . '/src')
|
|
|
|
return root
|
|
|
|
endif
|
|
|
|
|
|
|
|
" Search for 'src/'
|
|
|
|
let root = s:find_root_by('src', a:d)
|
|
|
|
" Check that 'spec/' is also there
|
|
|
|
if root !=# '' && isdirectory(root . '/spec')
|
|
|
|
return root
|
|
|
|
endif
|
|
|
|
|
|
|
|
" Cannot find a directory containing both 'src/' and 'spec/'
|
|
|
|
return ''
|
2016-05-02 04:49:45 -04:00
|
|
|
endfunction
|
|
|
|
|
|
|
|
function! crystal_lang#entrypoint_for(file_path) abort
|
|
|
|
let parent_dir = fnamemodify(a:file_path, ':p:h')
|
2019-03-29 15:30:36 -04:00
|
|
|
let root_dir = s:find_root_by_spec_and_src(parent_dir)
|
2016-05-02 04:49:45 -04:00
|
|
|
if root_dir ==# ''
|
2019-03-29 15:30:36 -04:00
|
|
|
" No spec directory found. No need to make temporary file
|
2016-05-02 04:49:45 -04:00
|
|
|
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
|
|
|
|
|
2018-01-10 17:50:02 -05:00
|
|
|
let impl = s:decode_json(cmd_result.output)
|
2016-05-02 04:49:45 -04:00
|
|
|
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
|
|
|
|
return s:find_completion_start()
|
|
|
|
endif
|
|
|
|
|
|
|
|
let cmd_result = crystal_lang#context(expand('%'), getpos('.'), '--format json')
|
|
|
|
if cmd_result.failed
|
|
|
|
return
|
|
|
|
endif
|
|
|
|
|
2018-01-10 17:50:02 -05:00
|
|
|
let contexts = s:decode_json(cmd_result.output)
|
2016-05-02 04:49:45 -04:00
|
|
|
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
|
2019-03-29 15:30:36 -04:00
|
|
|
let root_path = s:find_root_by_spec_and_src(path)
|
2016-05-02 04:49:45 -04:00
|
|
|
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
|
2019-03-29 15:30:36 -04:00
|
|
|
let root_dir = s:find_root_by_spec_and_src(source_dir)
|
2016-05-02 04:49:45 -04:00
|
|
|
if root_dir ==# ''
|
2019-03-29 15:30:36 -04:00
|
|
|
return s:echo_error("Root directory with 'src/' and 'spec/' not found")
|
2016-05-02 04:49:45 -04:00
|
|
|
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
|
|
|
|
|
2018-02-05 22:15:01 -05:00
|
|
|
" crystal_lang#format(option_str [, on_save])
|
|
|
|
function! crystal_lang#format(option_str, ...) abort
|
2016-05-02 04:49:45 -04:00
|
|
|
if !executable(g:crystal_compiler_command)
|
|
|
|
" Finish command silently
|
|
|
|
return
|
|
|
|
endif
|
|
|
|
|
2018-02-05 22:15:01 -05:00
|
|
|
let on_save = a:0 > 0 ? a:1 : 0
|
2016-05-02 04:49:45 -04:00
|
|
|
|
2018-02-05 22:15:01 -05:00
|
|
|
let before = join(getline(1, '$'), "\n")
|
|
|
|
let formatted = crystal_lang#format_string(before, a:option_str)
|
|
|
|
if !on_save
|
|
|
|
let after = substitute(formatted, '\n$', '', '')
|
|
|
|
if before ==# after
|
|
|
|
return
|
|
|
|
endif
|
|
|
|
endif
|
2016-05-02 04:49:45 -04:00
|
|
|
|
2018-02-05 22:15:01 -05:00
|
|
|
let view_save = winsaveview()
|
|
|
|
let pos_save = getpos('.')
|
|
|
|
let lines = split(formatted, '\n')
|
|
|
|
silent! undojoin
|
|
|
|
if line('$') > len(lines)
|
|
|
|
execute len(lines) . ',$delete' '_'
|
|
|
|
endif
|
|
|
|
call setline(1, lines)
|
|
|
|
call winrestview(view_save)
|
|
|
|
call setpos('.', pos_save)
|
2016-05-02 04:49:45 -04:00
|
|
|
endfunction
|
|
|
|
|
2018-10-08 13:00:59 -04:00
|
|
|
function! crystal_lang#expand(file, pos, ...) abort
|
|
|
|
return crystal_lang#tool('expand', a:file, a:pos, get(a:, 1, ''))
|
|
|
|
endfunction
|
|
|
|
|
2016-05-02 04:49:45 -04:00
|
|
|
let &cpo = s:save_cpo
|
|
|
|
unlet s:save_cpo
|