summaryrefslogtreecommitdiffstats
path: root/ftplugin/coffee.vim
blob: 701d4796029463f631d2f5e0e7b47cf7835f2f62 (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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
if has_key(g:polyglot_is_disabled, 'coffee-script')
  finish
endif

" Language:    CoffeeScript
" Maintainer:  Mick Koch <mick@kochm.co>
" URL:         http://github.com/kchmck/vim-coffee-script
" License:     WTFPL

if exists('b:did_ftplugin')
  finish
endif

let b:did_ftplugin = 1
call coffee#CoffeeSetUpVariables()

setlocal formatoptions-=t formatoptions+=croql
setlocal comments=:# commentstring=#\ %s
setlocal omnifunc=javascriptcomplete#CompleteJS
setlocal suffixesadd+=.coffee

" Create custom augroups.
augroup CoffeeBufUpdate | augroup END
augroup CoffeeBufNew | augroup END

" Enable coffee compiler if a compiler isn't set already.
if !len(&l:makeprg)
  compiler coffee
endif

" Switch to the window for buf.
function! s:SwitchWindow(buf)
  exec bufwinnr(a:buf) 'wincmd w'
endfunction

" Create a new scratch buffer and return the bufnr of it. After the function
" returns, vim remains in the scratch buffer so more set up can be done.
function! s:ScratchBufBuild(src, vert, size)
  if a:size <= 0
    if a:vert
      let size = winwidth(bufwinnr(a:src)) / 2
    else
      let size = winheight(bufwinnr(a:src)) / 2
    endif
  endif

  if a:vert
    vertical belowright new
    exec 'vertical resize' size
  else
    belowright new
    exec 'resize' size
  endif

  setlocal bufhidden=wipe buftype=nofile nobuflisted noswapfile nomodifiable
  nnoremap <buffer> <silent> q :hide<CR>

  return bufnr('%')
endfunction

" Replace buffer contents with text and delete the last empty line.
function! s:ScratchBufUpdate(buf, text)
  " Move to the scratch buffer.
  call s:SwitchWindow(a:buf)

  " Double check we're in the scratch buffer before overwriting.
  if bufnr('%') != a:buf
    throw 'unable to change to scratch buffer'
  endif

  setlocal modifiable
    silent exec '% delete _'
    silent put! =a:text
    silent exec '$ delete _'
  setlocal nomodifiable
endfunction

" Parse the output of coffee into a qflist entry for src buffer.
function! s:ParseCoffeeError(output, src, startline)
  " Coffee error is always on first line?
  let match = matchlist(a:output,
  \                     '^\(\f\+\|\[stdin\]\):\(\d\):\(\d\): error: \(.\{-}\)' . "\n")

  if !len(match)
    return
  endif

  " Consider the line number from coffee as relative and add it to the beginning
  " line number of the range the command was called on, then subtract one for
  " zero-based relativity.
  call setqflist([{'bufnr': a:src, 'lnum': a:startline + str2nr(match[2]) - 1,
  \                'type': 'E', 'col': str2nr(match[3]), 'text': match[4]}], 'r')
endfunction

" Reset source buffer variables.
function! s:CoffeeCompileResetVars()
  " Variables defined in source buffer:
  "   b:coffee_compile_buf: bufnr of output buffer
  " Variables defined in output buffer:
  "   b:coffee_src_buf: bufnr of source buffer
  "   b:coffee_compile_pos: previous cursor position in output buffer

  let b:coffee_compile_buf = -1
endfunction

function! s:CoffeeWatchResetVars()
  " Variables defined in source buffer:
  "   b:coffee_watch_buf: bufnr of output buffer
  " Variables defined in output buffer:
  "   b:coffee_src_buf: bufnr of source buffer
  "   b:coffee_watch_pos: previous cursor position in output buffer

  let b:coffee_watch_buf = -1
endfunction

function! s:CoffeeRunResetVars()
  " Variables defined in CoffeeRun source buffer:
  "   b:coffee_run_buf: bufnr of output buffer
  " Variables defined in CoffeeRun output buffer:
  "   b:coffee_src_buf: bufnr of source buffer
  "   b:coffee_run_pos: previous cursor position in output buffer

  let b:coffee_run_buf = -1
endfunction

" Clean things up in the source buffers.
function! s:CoffeeCompileClose()
  " Switch to the source buffer if not already in it.
  silent! call s:SwitchWindow(b:coffee_src_buf)
  call s:CoffeeCompileResetVars()
endfunction

function! s:CoffeeWatchClose()
  silent! call s:SwitchWindow(b:coffee_src_buf)
  silent! autocmd! CoffeeAuWatch * <buffer>
  call s:CoffeeWatchResetVars()
endfunction

function! s:CoffeeRunClose()
  silent! call s:SwitchWindow(b:coffee_src_buf)
  call s:CoffeeRunResetVars()
endfunction

" Compile the lines between startline and endline and put the result into buf.
function! s:CoffeeCompileToBuf(buf, startline, endline)
  let src = bufnr('%')
  let input = join(getline(a:startline, a:endline), "\n")

  " Coffee doesn't like empty input.
  if !len(input)
    " Function should still return within output buffer.
    call s:SwitchWindow(a:buf)
    return
  endif

  " Pipe lines into coffee.
  let output = system(g:coffee_compiler .
  \                   ' -scb' .
  \                   ' ' . b:coffee_litcoffee .
  \                   ' 2>&1', input)

  " Paste output into output buffer.
  call s:ScratchBufUpdate(a:buf, output)

  " Highlight as JavaScript if there were no compile errors.
  if v:shell_error
    call s:ParseCoffeeError(output, src, a:startline)
    setlocal filetype=
  else
    " Clear the quickfix list.
    call setqflist([], 'r')
    setlocal filetype=javascript
  endif
endfunction

" Peek at compiled CoffeeScript in a scratch buffer. We handle ranges like this
" to prevent the cursor from being moved (and its position saved) before the
" function is called.
function! s:CoffeeCompile(startline, endline, args)
  if a:args =~ '\<watch\>'
    echoerr 'CoffeeCompile watch is deprecated! Please use CoffeeWatch instead'
    sleep 5
    call s:CoffeeWatch(a:args)
    return
  endif

  " Switch to the source buffer if not already in it.
  silent! call s:SwitchWindow(b:coffee_src_buf)

  " Bail if not in source buffer.
  if !exists('b:coffee_compile_buf')
    return
  endif

  " Build the output buffer if it doesn't exist.
  if bufwinnr(b:coffee_compile_buf) == -1
    let src = bufnr('%')

    let vert = exists('g:coffee_compile_vert') || a:args =~ '\<vert\%[ical]\>'
    let size = str2nr(matchstr(a:args, '\<\d\+\>'))

    " Build the output buffer and save the source bufnr.
    let buf = s:ScratchBufBuild(src, vert, size)
    let b:coffee_src_buf = src

    " Set the buffer name.
    exec 'silent! file [CoffeeCompile ' . src . ']'

    " Clean up the source buffer when the output buffer is closed.
    autocmd BufWipeout <buffer> call s:CoffeeCompileClose()
    " Save the cursor when leaving the output buffer.
    autocmd BufLeave <buffer> let b:coffee_compile_pos = getpos('.')

    " Run user-defined commands on new buffer.
    silent doautocmd CoffeeBufNew User CoffeeCompile

    " Switch back to the source buffer and save the output bufnr. This also
    " triggers BufLeave above.
    call s:SwitchWindow(src)
    let b:coffee_compile_buf = buf
  endif

  " Fill the scratch buffer.
  call s:CoffeeCompileToBuf(b:coffee_compile_buf, a:startline, a:endline)
  " Reset cursor to previous position.
  call setpos('.', b:coffee_compile_pos)

  " Run any user-defined commands on the scratch buffer.
  silent doautocmd CoffeeBufUpdate User CoffeeCompile
endfunction

" Update the scratch buffer and switch back to the source buffer.
function! s:CoffeeWatchUpdate()
  call s:CoffeeCompileToBuf(b:coffee_watch_buf, 1, '$')
  call setpos('.', b:coffee_watch_pos)
  silent doautocmd CoffeeBufUpdate User CoffeeWatch
  call s:SwitchWindow(b:coffee_src_buf)
endfunction

" Continually compile a source buffer.
function! s:CoffeeWatch(args)
  silent! call s:SwitchWindow(b:coffee_src_buf)

  if !exists('b:coffee_watch_buf')
    return
  endif

  if bufwinnr(b:coffee_watch_buf) == -1
    let src = bufnr('%')

    let vert = exists('g:coffee_watch_vert') || a:args =~ '\<vert\%[ical]\>'
    let size = str2nr(matchstr(a:args, '\<\d\+\>'))

    let buf = s:ScratchBufBuild(src, vert, size)
    let b:coffee_src_buf = src

    exec 'silent! file [CoffeeWatch ' . src . ']'

    autocmd BufWipeout <buffer> call s:CoffeeWatchClose()
    autocmd BufLeave <buffer> let b:coffee_watch_pos = getpos('.')

    silent doautocmd CoffeeBufNew User CoffeeWatch

    call s:SwitchWindow(src)
    let b:coffee_watch_buf = buf
  endif

  " Make sure only one watch autocmd is defined on this buffer.
  silent! autocmd! CoffeeAuWatch * <buffer>

  augroup CoffeeAuWatch
    autocmd InsertLeave <buffer> call s:CoffeeWatchUpdate()
    autocmd BufWritePost <buffer> call s:CoffeeWatchUpdate()
  augroup END

  call s:CoffeeWatchUpdate()
endfunction

" Run a snippet of CoffeeScript between startline and endline.
function! s:CoffeeRun(startline, endline, args)
  silent! call s:SwitchWindow(b:coffee_src_buf)

  if !exists('b:coffee_run_buf')
    return
  endif

  if bufwinnr(b:coffee_run_buf) == -1
    let src = bufnr('%')

    let buf = s:ScratchBufBuild(src, exists('g:coffee_run_vert'), 0)
    let b:coffee_src_buf = src

    exec 'silent! file [CoffeeRun ' . src . ']'

    autocmd BufWipeout <buffer> call s:CoffeeRunClose()
    autocmd BufLeave <buffer> let b:coffee_run_pos = getpos('.')

    silent doautocmd CoffeeBufNew User CoffeeRun

    call s:SwitchWindow(src)
    let b:coffee_run_buf = buf
  endif

  if a:startline == 1 && a:endline == line('$')
    let output = system(g:coffee_compiler .
    \                   ' ' . b:coffee_litcoffee .
    \                   ' ' . fnameescape(expand('%')) .
    \                   ' ' . a:args)
  else
    let input = join(getline(a:startline, a:endline), "\n")

    if !len(input)
      return
    endif

    let output = system(g:coffee_compiler .
    \                   ' -s' .
    \                   ' ' . b:coffee_litcoffee .
    \                   ' ' . a:args, input)
  endif

  call s:ScratchBufUpdate(b:coffee_run_buf, output)
  call setpos('.', b:coffee_run_pos)

  silent doautocmd CoffeeBufUpdate User CoffeeRun
endfunction

" Run coffeelint on a file, and add any errors between startline and endline
" to the quickfix list.
function! s:CoffeeLint(startline, endline, bang, args)
  let input = join(getline(a:startline, a:endline), "\n")

  if !len(input)
    return
  endif

  let output = system(g:coffee_linter .
  \                   ' -s --reporter csv' .
  \                   ' ' . b:coffee_litcoffee .
  \                   ' ' . g:coffee_lint_options .
  \                   ' ' . a:args .
  \                   ' 2>&1', input)

  " Convert output into an array and strip off the csv header.
  let lines = split(output, "\n")[1:]
  let buf = bufnr('%')
  let qflist = []

  for line in lines
    let match = matchlist(line, '^stdin,\(\d\+\),\d*,\(error\|warn\),\(.\+\)$')

    " Ignore unmatched lines.
    if !len(match)
      continue
    endif

    " The 'type' will result in either 'E' or 'W'.
    call add(qflist, {'bufnr': buf, 'lnum': a:startline + str2nr(match[1]) - 1,
    \                 'type': toupper(match[2][0]), 'text': match[3]})
  endfor

  " Replace the quicklist with our items.
  call setqflist(qflist, 'r')

  " If not given a bang, jump to first error.
  if !len(a:bang)
    silent! cc 1
  endif
endfunction

" Complete arguments for Coffee* commands.
function! s:CoffeeComplete(cmd, cmdline, cursor)
  let args = ['vertical']

  " If no partial command, return all possibilities.
  if !len(a:cmd)
    return args
  endif

  let pat = '^' . a:cmd

  for arg in args
    if arg =~ pat
      return [arg]
    endif
  endfor
endfunction

" Set initial state variables if they don't exist
if !exists('b:coffee_compile_buf')
  call s:CoffeeCompileResetVars()
endif

if !exists('b:coffee_watch_buf')
  call s:CoffeeWatchResetVars()
endif

if !exists('b:coffee_run_buf')
  call s:CoffeeRunResetVars()
endif

command! -buffer -range=% -bar -nargs=* -complete=customlist,s:CoffeeComplete
\        CoffeeCompile call s:CoffeeCompile(<line1>, <line2>, <q-args>)
command! -buffer -bar -nargs=* -complete=customlist,s:CoffeeComplete
\        CoffeeWatch call s:CoffeeWatch(<q-args>)
command! -buffer -range=% -bar -nargs=* CoffeeRun
\        call s:CoffeeRun(<line1>, <line2>, <q-args>)
command! -buffer -range=% -bang -bar -nargs=* CoffeeLint
\        call s:CoffeeLint(<line1>, <line2>, <q-bang>, <q-args>)