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

if exists("b:did_ftplugin")
  finish
endif

let b:did_ftplugin = 1

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

" Enable CoffeeMake if it won't overwrite any settings.
if !len(&l:makeprg)
  compiler coffee
endif

" Check here too in case the compiler above isn't loaded.
if !exists('coffee_compiler')
  let coffee_compiler = 'coffee'
endif

" Path to coffeelint executable
if !exists('coffee_linter')
  let coffee_linter = 'coffeelint'
endif

" Options passed to CoffeeLint
if !exists('coffee_lint_options')
  let coffee_lint_options = ''
endif

" Reset the CoffeeCompile variables for the current buffer.
function! s:CoffeeCompileResetVars()
  " Compiled output buffer
  let b:coffee_compile_buf = -1
  let b:coffee_compile_pos = []

  " If CoffeeCompile is watching a buffer
  let b:coffee_compile_watch = 0
endfunction

" Clean things up in the source buffer.
function! s:CoffeeCompileClose()
  exec bufwinnr(b:coffee_compile_src_buf) 'wincmd w'
  silent! autocmd! CoffeeCompileAuWatch * <buffer>
  call s:CoffeeCompileResetVars()
endfunction

" Update the CoffeeCompile buffer given some input lines.
function! s:CoffeeCompileUpdate(startline, endline)
  let input = join(getline(a:startline, a:endline), "\n")

  " Move to the CoffeeCompile buffer.
  exec bufwinnr(b:coffee_compile_buf) 'wincmd w'

  " Coffee doesn't like empty input.
  if !len(input)
    return
  endif

  " Compile input.
  let output = system(g:coffee_compiler . ' -scb 2>&1', input)

  " Be sure we're in the CoffeeCompile buffer before overwriting.
  if exists('b:coffee_compile_buf')
    echoerr 'CoffeeCompile buffers are messed up'
    return
  endif

  " Replace buffer contents with new output and delete the last empty line.
  setlocal modifiable
    exec '% delete _'
    put! =output
    exec '$ delete _'
  setlocal nomodifiable

  " Highlight as JavaScript if there is no compile error.
  if v:shell_error
    setlocal filetype=
  else
    setlocal filetype=javascript
  endif

  call setpos('.', b:coffee_compile_pos)
endfunction

" Update the CoffeeCompile buffer with the whole source buffer.
function! s:CoffeeCompileWatchUpdate()
  call s:CoffeeCompileUpdate(1, '$')
  exec bufwinnr(b:coffee_compile_src_buf) 'wincmd w'
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 !executable(g:coffee_compiler)
    echoerr "Can't find CoffeeScript compiler `" . g:coffee_compiler . "`"
    return
  endif

  " If in the CoffeeCompile buffer, switch back to the source buffer and
  " continue.
  if !exists('b:coffee_compile_buf')
    exec bufwinnr(b:coffee_compile_src_buf) 'wincmd w'
  endif

  " Parse arguments.
  let watch = a:args =~ '\<watch\>'
  let unwatch = a:args =~ '\<unwatch\>'
  let size = str2nr(matchstr(a:args, '\<\d\+\>'))

  " Determine default split direction.
  if exists('g:coffee_compile_vert')
    let vert = 1
  else
    let vert = a:args =~ '\<vert\%[ical]\>'
  endif

  " Remove any watch listeners.
  silent! autocmd! CoffeeCompileAuWatch * <buffer>

  " If just unwatching, don't compile.
  if unwatch
    let b:coffee_compile_watch = 0
    return
  endif

  if watch
    let b:coffee_compile_watch = 1
  endif

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

    " Create the new window and resize it.
    if vert
      let width = size ? size : winwidth(src_win) / 2

      belowright vertical new
      exec 'vertical resize' width
    else
      " Try to guess the compiled output's height.
      let height = size ? size : min([winheight(src_win) / 2,
      \                               a:endline - a:startline + 2])

      belowright new
      exec 'resize' height
    endif

    " We're now in the scratch buffer, so set it up.
    setlocal bufhidden=wipe buftype=nofile
    setlocal nobuflisted nomodifiable noswapfile nowrap

    autocmd BufWipeout <buffer> call s:CoffeeCompileClose()
    " Save the cursor when leaving the CoffeeCompile buffer.
    autocmd BufLeave <buffer> let b:coffee_compile_pos = getpos('.')

    nnoremap <buffer> <silent> q :hide<CR>

    let b:coffee_compile_src_buf = src_buf
    let buf = bufnr('%')

    " Go back to the source buffer and set it up.
    exec bufwinnr(b:coffee_compile_src_buf) 'wincmd w'
    let b:coffee_compile_buf = buf
  endif

  if b:coffee_compile_watch
    call s:CoffeeCompileWatchUpdate()

    augroup CoffeeCompileAuWatch
      autocmd InsertLeave <buffer> call s:CoffeeCompileWatchUpdate()
    augroup END
  else
    call s:CoffeeCompileUpdate(a:startline, a:endline)
  endif
endfunction

" Complete arguments for the CoffeeCompile command.
function! s:CoffeeCompileComplete(arg, cmdline, cursor)
  let args = ['unwatch', 'vertical', 'watch']

  if !len(a:arg)
    return args
  endif

  let match = '^' . a:arg

  for arg in args
    if arg =~ match
      return [arg]
    endif
  endfor
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)
  if !executable(g:coffee_linter)
    echoerr "Can't find CoffeeScript linter `" . g:coffee_linter . "`"
    return
  endif

  let filename = expand('%')

  if !len(filename)
    echoerr 'CoffeeLint must be ran on a saved file'
    return
  endif

  let lines = split(system(g:coffee_linter . ' --csv ' . g:coffee_lint_options .
  \                        ' ' . a:args . ' ' . filename . ' 2>&1'), '\n')
  let qflist = []

  for line in lines
    let match = matchlist(line, '\f\+,\(\d\+\),error,\(.\+\)')

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

    let lnum = str2nr(match[1])

    " Don't add the error if it's not in the range.
    if lnum < a:startline || lnum > a:endline
      continue
    endif

    let text = match[2]

    call add(qflist, {'bufnr': bufnr('%'), 'lnum': lnum, 'text': text})
  endfor

  call setqflist(qflist, 'r')

  " Don't jump if there's a bang.
  if !len(a:bang)
    silent! cc 1
  endif
endfunction

" Don't overwrite the CoffeeCompile variables.
if !exists('b:coffee_compile_buf')
  call s:CoffeeCompileResetVars()
endif

" Peek at compiled CoffeeScript.
command! -range=% -bar -nargs=* -complete=customlist,s:CoffeeCompileComplete
\        CoffeeCompile call s:CoffeeCompile(<line1>, <line2>, <q-args>)
" Run some CoffeeScript.
command! -range=% -bar CoffeeRun <line1>,<line2>:w !coffee -s
" Run coffeelint on the file.
command! -range=% -bang -bar -nargs=* CoffeeLint
\        call s:CoffeeLint(<line1>, <line2>, '<bang>', <q-args>)