summaryrefslogtreecommitdiffstats
path: root/autoload/crystal_lang.vim
blob: b0e63ea260d99412c5edb2f1ea417cf52e042dfc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1

let s:V = vital#crystal#new()
let s:P = s:V.import('Process')
let s:C = s:V.import('ColorEcho')

let s:IS_WINDOWS = has('win32')

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

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(search_dir, d) abort
  let found_dir = finddir(a:search_dir, a:d . ';')
  if found_dir ==# ''
    return ''
  endif

  " 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 ''
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_and_src(parent_dir)
  if root_dir ==# ''
    " No spec directory found. No need to make temporary file
    return a:file_path
  endif

  let required_spec_path = get(b:, 'crystal_required_spec_path', get(g:, 'crystal_required_spec_path', ''))
  if required_spec_path !=# ''
    let require_spec_str = './' . required_spec_path
  else
    let require_spec_str = './spec/**'
  endif

  let temp_name = root_dir . '/__vim-crystal-temporary-entrypoint-' . fnamemodify(a:file_path, ':t')
  let contents = [
        \   'require "spec"',
        \   'require "' . require_spec_str . '"',
        \   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:decode_json(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
    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:decode_json(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_and_src(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_and_src(source_dir)
  if root_dir ==# ''
    return s:echo_error("Root directory with 'src/' and 'spec/' 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
  if s:IS_WINDOWS
    let redirect = '2> nul'
  else
    let redirect = '2>/dev/null'
  endif
  let cmd = printf(
        \   '%s tool format --no-color %s - %s',
        \   g:crystal_compiler_command,
        \   get(a:, 1, ''),
        \   redirect,
        \ )
  let output = s:P.system(cmd, a:code)
  if s:P.get_last_status()
    throw 'vim-crystal: Error on formatting with command: ' . cmd
  endif
  return output
endfunction

" crystal_lang#format(option_str [, on_save])
function! crystal_lang#format(option_str, ...) abort
  let on_save = a:0 > 0 ? a:1 : 0

  if !executable(g:crystal_compiler_command)
    if on_save
      " Finish command silently on save
      return
    else
      throw 'vim-crystal: Command for formatting is not executable: ' . g:crystal_compiler_command
    endif
  endif

  let before = join(getline(1, '$'), "\n")
  try
    let formatted = crystal_lang#format_string(before, a:option_str)
  catch /^vim-crystal: /
    echohl ErrorMsg
    echomsg v:exception . ': Your code was not formatted. Exception was thrown at ' . v:throwpoint
    echohl None
    return
  endtry

  if !on_save
    let after = substitute(formatted, '\n$', '', '')
    if before ==# after
      return
    endif
  endif

  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)
endfunction

function! crystal_lang#expand(file, pos, ...) abort
  return crystal_lang#tool('expand', a:file, a:pos, get(a:, 1, ''))
endfunction

" vim: sw=2 sts=2 et:

endif