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