packages feed

scion-0.1: vim_runtime_path/autoload/haskellcomplete.vim

" This file contains the code necessary to talk to the scion server
" -> haskellcomplete#EvalScion )
"
" This implementation requires has('python') support

" You can look up some use cases in the ftplugin file.
"
" This code is based on the initial implementation found in shim by Benedikt Schmidt
" The server side code can be found in src-scion-server/Scion/Server/ProtocolVim.hs

if exists('g:tovl')
  fun! s:Log(level, msg)
    call tovl#log#Log("haskellcomplete",a:level, type(a:msg) == type("") ? a:msg : string(a:msg))
  endf
else
  fun! s:Log(level, msg)
    echoe a:msg
  endf
endif

" require python or exit
if !has('python') | call s:Log(0, "Error: scion requires vim compiled with +python") | finish | endif

let g:vim_scion_protocol_version = "0"

fun! haskellcomplete#LoadComponent(set_cabal_project, component)
  let result = haskellcomplete#EvalScion(0, 'load-component', { 'component' : a:component})
  if has_key(result,'error')
    if result['error']['message'] == "NoCurrentCabalProject" && a:set_cabal_project
      let cabal_project = haskellcomplete#SetCurrentCabalProject()
      return haskellcomplete#LoadComponent(0, a:component)
    else
      throw "can't handle this failure: ".string(result['error'])
    endif
  endif
  return result['result']
endf

fun! haskellcomplete#SetCurrentCabalProject()
  let configs = haskellcomplete#EvalScion(1,'list-cabal-configurations',
    \ { 'cabal-file' : haskellcomplete#CabalFile()
    \ , 'scion-default': json#IntToBool(get(g:scion_config, "use_default_scion_cabal_dist_dir", 1)) })
  let config = haskellcomplete#ChooseFromList(configs, 'select a cabal configuration')
  let result = haskellcomplete#EvalScion(1,'open-cabal-project'
              \  ,{'root-dir' : getcwd()
              \   ,'dist-dir' : config['dist-dir']
              \   ,'extra-args' : config['extra-args'] }
              \ )
endf

fun! haskellcomplete#ScionResultToErrorList(action, func, result)
  let qflist = []
  for dict in a:result['notes']
    let loc = dict['location']
    if has_key(loc, 'no-location')
      " using no-location so that we have an item to jump to.
      " ef we don't use that dummy file SaneHook won't see any errors!
      call add(qflist, { 'filename' : 'no-location'
              \ ,'lnum' : 0
              \ ,'col'  : 0
              \ ,'text' : loc['no-location']
              \ ,'type' : dict['kind'] == "error" ? "E" : "W"
              \ })
    else
      call add(qflist, { 'filename' : loc['file']
              \ ,'lnum' : loc['region'][0]
              \ ,'col'  : loc['region'][1]
              \ ,'text' : ''
              \ ,'type' : dict['kind'] == "error" ? "E" : "W"
              \ })
    endif
    for msgline in split(dict['message'],"\n")
      call add(qflist, {'text': msgline})
    endfor
  endfor
  
  call call(a:func, [qflist])
  if exists('g:haskell_qf_hook')
    exec g:haskell_qf_hook
  endif
  if (len(qflist) == 0)
    return printf(a:action." success. compilationTime: %s", string(a:result['duration']))
  else
    return printf(a:action." There are errors. compilationTime: %s", string(a:result['duration']))
  endif
endfun


" if there is item take it, if there are more than one ask user which one to
" use.. -- don't think cabal allows multiple .cabal files.. At least the user
" is notified that there are more than one .cabal files
fun! haskellcomplete#ChooseFromList(list, ...)
    let msg = a:0 > 0 ? a:1 : "choose from list"
    if empty(a:list)
      return
    elseif len(a:list) == 1
      return a:list[0]
    else
      let l = []
      let i = 1
      for line in a:list
        let line2 = type(line) != type('') ? string(line) : line
        call add(l, i.': '.line2)
        let i = i + 1
        unlet line
      endfor
      return a:list[inputlist(l)-1]
    endif
endf

fun! haskellcomplete#CabalFile()
  if !exists('g:cabal_file')
    let list = split(glob('*.cabal'),"\n")
    if empty(list)
      throw "no cabal file found"
    endif
    let g:cabal_file = getcwd().'/'.haskellcomplete#ChooseFromList(list)
  endif
  return g:cabal_file
endf

fun! haskellcomplete#List(what)
  return haskellcomplete#EvalScion(1,'list-'.a:what, {})
endf

fun! haskellcomplete#OpenCabalProject(method, ...)
  return haskellcomplete#EvalScion(1,a:method
    \  ,{'root-dir' : getcwd()
    \   ,'dist-dir' : a:1
    \   ,'extra-args' : a:000[1:] }
    \ )
endf

fun! haskellcomplete#compToV(...)
  let component = a:0 > 0 ? a:1 : 'file:'.expand('%:p')
  let m = matchstr(component, '^executable:\zs.*')
  if m != '' | return {'executable' : m} | endif
  let m = matchstr(component, '^library$')
  if m != '' | return {'library' : json#NULL()} | endif
  let m = matchstr(component, '^file:\zs.*')
  if m != '' | return {'file' : m} | endif
  throw "invalid component".component
endfun

fun! haskellcomplete#WriteSampleConfig(...)
  let file = a:0 > 0 ? a:1 : ".scion-config"
  let file = fnamemodify(file, ":p")
  return haskellcomplete#EvalScion(1, 'write-sample-config', {'file' : file})
endf

" if there are errors open quickfix and jump to first error (ignoring warning)
" if not close it
fun! haskellcomplete#SaneHook()
  let list = getqflist()
  let nr = 0
  let open = 0
  let firstError = 0
  for i in getqflist()
    let nr = nr +1
    if i['bufnr'] == 0 | continue | endif
    if i['type'] == "E" && firstError == 0
      let firstError = nr
    endif
    let open = 1
  endfor
  if open
    cope " open
    " move to first error
    if firstError > 0 | exec "crewind ".firstError | endif
  else
    cclose
  endif
endf

" use this to connect to a socket
" connection settings: see strings in connectscion

" returns string part before and after cursor
function! haskellcomplete#BcAc()
  let pos = col('.') -1
  let line = getline('.')
  return [strpart(line,0,pos), strpart(line, pos, len(line)-pos)]
endfunction

" completion functions
if !exists('g:haskellcompleteAll')
  let g:haskellcompleteAll='' " '' or '-all'  '-all' means complete from the set of all function exported by all modules found in all used packages
endif

fun! haskellcomplete#CompletModule(findstart, base)
  if a:findstart
    let [bc,ac] = haskellcomplete#BcAc()
    return len(bc)-len(matchstr(bc,'\S*$'))
  else
    let [bc,ac] = haskellcomplete#BcAc()
    let addImport = bc !~ 'import\s\+\S*$'
    let matches = haskellcomplete#EvalScion(
      \ { 'request' : 'cmdModuleCompletion'
      \ , 'camelCase' : 'True'
      \ , 'short' : a:base
      \ })
    if addImport
      call map(matches, string('import ').'.v:val')
    endif
    return matches
  endif
endf

let g:scion_request_id = 1

" name: method name
" params: params to method
" optional argument: continuation function (not yet implemented)
" returns: nothing when continuation function is given
"          reply else
function! haskellcomplete#EvalScion(fail_on_error, method, params, ...)
  if a:0 > 0
    let continuation a:0
  endif
  let g:scion_request_id = g:scion_request_id + 1
  let request = { 'method' : a:method, 'params' : a:params, 'id' : g:scion_request_id }
  " the first string converts the vim object into a string, the second
  " converts this string into a python string
  let g:scion_arg = json#Encode(request)
  py evalscionAssign(vim.eval('g:scion_arg'))
  " warnings
  for w in get(g:scion_result, 'warnings', [])
    call s:Log(1, w) | echo w
  endfor
  " errors

  if !a:fail_on_error
    return g:scion_result
  endif

  if has_key(g:scion_result,'error')
    call s:Log(0, g:scion_result['error'])
    throw "There was a scion server error :".string(g:scion_result['error'])
  else
    return g:scion_result['result']
  endif
endfunction

function! s:DefPython()
python << PYTHONEOF
import sys, tokenize, cStringIO, types, socket, string, vim, popen2, os
from subprocess import Popen, PIPE

scion_log_stdout = vim.eval('exists("g:scion_log_stdout") && g:scion_log_stdout')
scion_stdout = []

class ScionServerConnection:
  """base of a server connection. They all provide two methods: send and receive bothe sending or receiving a single line separated by \\n"""
  def send(self, line):
    self.scion_i.write("%s\n"%line)
    self.scion_i.flush()
  def receive(self):
    s = self.scion_o.readline()
    if s == "":
      raise "EOF, stderr lines: \n%s"%self.scion_err.read()
    else:
      return s[:-1]

class ScionServerConnectionStdinOut(ScionServerConnection):
  """this connection launches the server and connects to its stdin and stdout streams"""
  def __init__(self, scion_executable):
    #self.scion_o,self.scion_i,e = popen2.popen3('%s -i -f /tmp/scion-log-%s' % (scion_executable, os.getcwd().replace('/','_').replace('\\','_'))
    p = Popen([scion_executable,"-i","-f", "/tmp/scion-log-%s"%(os.getcwd().replace('/','_').replace('\\','_'))], \
            shell = False, bufsize = 1, stdin = PIPE, stdout = PIPE, stderr = PIPE)
    self.scion_o = p.stdout
    self.scion_i = p.stdin
    self.scion_err = p.stderr

  def receive(self):
    global scion_log_stdout, scion_stdout
    s = ScionServerConnection.receive(self)

    # ghc doesn't always use the ghc API to print statements.. so ignore all
    # lines not marked by "scion:" at the beginning
    # see README.markdown
    while s[:6] != "scion:":
      # throw away non "scion:" line and try again
      if scion_log_stdout:
        scion_stdout.append(s)
        scion_stdout = scion_stdout[-200:]        
      # should this be printed? It doesn't hurt much but might be useful when
      # trouble shooting..
      print "ignoring line", s
      s = ScionServerConnection.receive(self)

    return s[6:]

class ScionServerConnectionSocket(ScionServerConnection):
  """connects to the scion server by either TCP/IP or socketfile"""
  def __init__(self, connection):
    if type(connection) == type([]):
      # array [ host, port ]
      # vim.eval always returns strings!
      connection = (connection[0], string.atoi(connection[1]))
      print "connection is now %s" % connection.__str__()
      su = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    else: # must be path -> file socket
      print "else "
      su = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    su.settimeout(10)
    su.connect(connection)
    # making file to use readline()
    self.scion_o = su.makefile('rw')
    self.scion_i = self.scion_o

server_connection = None
told_user_about_missing_configuration = 0
lastScionResult = "";
def connectscion():
    # check that connection method has been defined
    global server_connection
    global told_user_about_missing_configuration
    if 0 == told_user_about_missing_configuration:
      try:
        # use connection form vim value so that python is used as lazily as possible
        scionConnectionSetting = vim.eval('g:scion_connection_setting')
        print "connecting to scion %s"%scionConnectionSetting.__str__()
      except NameError:
        vim.command("sp")
        b = vim.current.buffer
        b.append( "you haven't defined g:scion_connection_setting")
        b.append( "Do so by adding one of the following lines to your .vimrc:")
        b.append( "TCP/IP, socket, stdio")
        b.append( "let g:scion_connection_setting = ['socket', \"socket file location\"] # socket connection")
        b.append( "let g:scion_connection_setting = ['socket', ['localhost', 4005]] # host, port TCIP/IP connection")
        b.append( "let g:scion_connection_setting = ['scion', \"scion_server location\"] # stdio connection ")
        told_user_about_missing_configuration = 1

    if scionConnectionSetting[0] == "socket":
      server_connection = ScionServerConnectionSocket(scionConnectionSetting[1])
    else:
      server_connection = ScionServerConnectionStdinOut(scionConnectionSetting[1])
    # tell server than vim doesn't like true, false, null
    vim.command('call haskellcomplete#EvalScion(1, "client-identify", {"name":"vim"})')

# sends a command and returns the returned line
def evalscion(str):
    global server_connection
    try:
      server_connection.send(str)
    except:
      vim.command('echom "%s"'% ("(re)connecting to scion"))
      connectscion()
      server_connection.send(str)
    return server_connection.receive()

# str see EvalScion
def evalscionAssign(str):
  global lastScionResult
  """assigns scion result to g:scion_result, result should either be 
    { "result" : ..., "error" : [String] }"""
  vim.command("silent! unlet g:scion_result")
  lastScionResult = evalscion(str)
  vim.command("silent! let g:scion_result = %s" % lastScionResult)
  vim.command("if !exists('g:scion_result') | let g:scion_result = {'error' : \"couldn't parse scion result.\n%s\nTry :py print lastScionResult to see full server response\" } | endif " % str[:80])

# sys.path.extend(['.','..'])
PYTHONEOF
endfunction

call s:DefPython()
" vim: set et ts=4: