" fireplace.vim - Clojure REPL tease
" Maintainer: Tim Pope
if exists("g:loaded_fireplace") || v:version < 700 || &cp
finish
endif
let g:loaded_fireplace = 1
" Section: File type
augroup fireplace_file_type
autocmd!
autocmd BufNewFile,BufReadPost *.clj setfiletype clojure
augroup END
" Section: Escaping
function! s:str(string) abort
return '"' . escape(a:string, '"\') . '"'
endfunction
function! s:qsym(symbol) abort
if a:symbol =~# '^[[:alnum:]?*!+/=<>.:-]\+$'
return "'".a:symbol
else
return '(symbol '.s:str(a:symbol).')'
endif
endfunction
function! s:to_ns(path) abort
return tr(substitute(a:path, '\.\w\+$', '', ''), '\/_', '..-')
endfunction
" Section: Completion
let s:jar_contents = {}
function! fireplace#jar_contents(path) abort
if !exists('s:zipinfo')
if executable('zipinfo')
let s:zipinfo = 'zipinfo -1 '
elseif executable('python')
let s:zipinfo = 'python -c '.shellescape('import zipfile, sys; print chr(10).join(zipfile.ZipFile(sys.argv[1]).namelist())').' '
else
let s:zipinfo = ''
endif
endif
if !has_key(s:jar_contents, a:path) && has('python')
python import vim, zipfile
python vim.command("let s:jar_contents[a:path] = split('" + "\n".join(zipfile.ZipFile(vim.eval('a:path')).namelist()) + "', \"\n\")")
elseif !has_key(s:jar_contents, a:path) && !empty(s:zipinfo)
let s:jar_contents[a:path] = split(system(s:zipinfo.shellescape(a:path)), "\n")
if v:shell_error
let s:jar_contents[a:path] = []
endif
endif
return copy(get(s:jar_contents, a:path, []))
endfunction
function! fireplace#eval_complete(A, L, P) abort
let prefix = matchstr(a:A, '\%(.* \|^\)\%(#\=[\[{('']\)*')
let keyword = a:A[strlen(prefix) : -1]
return sort(map(fireplace#omnicomplete(0, keyword), 'prefix . v:val.word'))
endfunction
function! fireplace#ns_complete(A, L, P) abort
let matches = []
for dir in fireplace#path()
if dir =~# '\.jar$'
let files = filter(fireplace#jar_contents(dir), 'v:val =~# "\\.clj$"')
else
let files = split(glob(dir."/**/*.clj", 1), "\n")
call map(files, 'v:val[strlen(dir)+1 : -1]')
endif
let matches += files
endfor
return filter(map(matches, 's:to_ns(v:val)'), 'a:A ==# "" || a:A ==# v:val[0 : strlen(a:A)-1]')
endfunction
function! fireplace#omnicomplete(findstart, base) abort
if a:findstart
let line = getline('.')[0 : col('.')-2]
return col('.') - strlen(matchstr(line, '\k\+$')) - 1
else
try
if fireplace#op_available('complete')
let response = fireplace#message({'op': 'complete', 'symbol': a:base})
if type(get(response[0], 'value')) == type([])
if type(get(response[0].value, 0)) == type([])
return map(response[0].value[0], '{"word": v:val}')
elseif type(get(response[0].value, 0)) == type('')
return map(response[0].value, '{"word": v:val}')
else
return []
endif
endif
endif
let omnifier = '(fn [[k v]] (let [{:keys [arglists] :as m} (meta v)]' .
\ ' {:word k :menu (pr-str (or arglists (symbol ""))) :info (str (when arglists (str arglists "\n")) " " (:doc m)) :kind (if arglists "f" "v")}))'
let ns = fireplace#ns()
let [aliases, namespaces, maps] = fireplace#evalparse(
\ '[(ns-aliases '.s:qsym(ns).') (all-ns) '.
\ '(sort-by :word (map '.omnifier.' (ns-map '.s:qsym(ns).')))]')
if a:base =~# '^[^/]*/[^/]*$'
let ns = matchstr(a:base, '^.*\ze/')
let prefix = ns . '/'
let ns = get(aliases, ns, ns)
let keyword = matchstr(a:base, '.*/\zs.*')
let results = fireplace#evalparse(
\ '(sort-by :word (map '.omnifier.' (ns-publics '.s:qsym(ns).')))')
for r in results
let r.word = prefix . r.word
endfor
else
let keyword = a:base
let results = maps + map(sort(keys(aliases) + namespaces), '{"word": v:val."/", "kind": "t", "info": ""}')
endif
if type(results) == type([])
return filter(results, 'a:base ==# "" || a:base ==# v:val.word[0 : strlen(a:base)-1]')
else
return []
endif
catch /.*/
return []
endtry
endif
endfunction
augroup fireplace_completion
autocmd!
autocmd FileType clojure setlocal omnifunc=fireplace#omnicomplete
augroup END
" Section: REPL client
let s:repl = {"requires": {}}
if !exists('s:repls')
let s:repls = []
let s:repl_paths = {}
let s:repl_portfiles = {}
endif
function! s:repl.user_ns() abort
return 'user'
endfunction
function! s:repl.path() dict abort
return self.connection.path()
endfunction
function! s:conn_try(connection, function, ...) abort
try
return call(a:connection[a:function], a:000, a:connection)
catch /^\w\+ Connection Error:/
call s:unregister_connection(a:connection)
throw v:exception
endtry
endfunction
function! s:repl.eval(expr, options) dict abort
if has_key(a:options, 'ns') && a:options.ns !=# self.user_ns()
let error = self.preload(a:options.ns)
if !empty(error)
return error
endif
endif
return s:conn_try(self.connection, 'eval', a:expr, a:options)
endfunction
function! s:repl.message(payload, ...) dict abort
if has_key(a:payload, 'ns') && a:payload.ns !=# self.user_ns()
let ignored_error = self.preload(a:payload.ns)
endif
return call('s:conn_try', [self.connection, 'message', a:payload] + a:000, self)
endfunction
function! s:repl.preload(lib) dict abort
if !empty(a:lib) && a:lib !=# self.user_ns() && !get(self.requires, a:lib)
let reload = has_key(self.requires, a:lib) ? ' :reload' : ''
let self.requires[a:lib] = 0
let clone = s:conn_try(self.connection, 'clone')
if self.user_ns() ==# 'user'
let qsym = s:qsym(a:lib)
let expr = '(when-not (find-ns '.qsym.') (try'
\ . ' (#''clojure.core/load-one '.qsym.' true true)'
\ . ' (catch Exception e (when-not (find-ns '.qsym.') (throw e)))))'
else
let expr = '(ns '.self.user_ns().' (:require '.a:lib.reload.'))'
endif
try
let result = clone.eval(expr, {'ns': self.user_ns()})
finally
call clone.close()
endtry
let self.requires[a:lib] = !has_key(result, 'ex')
if has_key(result, 'ex')
return result
endif
endif
return {}
endfunction
let s:piggieback = copy(s:repl)
function! s:repl.piggieback(arg, ...) abort
if a:0 && a:1
if len(self.piggiebacks)
call remove(self.piggiebacks, 0)
endif
return {}
endif
let connection = s:conn_try(self.connection, 'clone')
if empty(a:arg)
let arg = ''
elseif a:arg =~# '^\d\{1,5}$'
call connection.eval("(require 'cljs.repl.browser)")
let port = matchstr(a:arg, '^\d\{1,5}$')
let arg = ' :repl-env (cljs.repl.browser/repl-env :port '.port.')'
else
let arg = ' :repl-env ' . a:arg
endif
let response = connection.eval('(cemerick.piggieback/cljs-repl'.arg.')')
if empty(get(response, 'ex'))
call insert(self.piggiebacks, extend({'connection': connection}, deepcopy(s:piggieback)))
return {}
endif
call connection.close()
return response
endfunction
function! s:piggieback.user_ns() abort
return 'cljs.user'
endfunction
function! s:piggieback.eval(expr, options) abort
let options = copy(a:options)
if has_key(options, 'file_path')
call remove(options, 'file_path')
endif
return call(s:repl.eval, [a:expr, options], self)
endfunction
function! s:register_connection(conn, ...) abort
call insert(s:repls, extend({'connection': a:conn, 'piggiebacks': []}, deepcopy(s:repl)))
if a:0 && a:1 !=# ''
let s:repl_paths[a:1] = s:repls[0]
endif
return s:repls[0]
endfunction
function! s:unregister_connection(conn) abort
call filter(s:repl_paths, 'v:val.connection.transport isnot# a:conn.transport')
call filter(s:repls, 'v:val.connection.transport isnot# a:conn.transport')
call filter(s:repl_portfiles, 'v:val.connection.transport isnot# a:conn.transport')
endfunction
function! fireplace#register_port_file(portfile, ...) abort
let old = get(s:repl_portfiles, a:portfile, {})
if has_key(old, 'time') && getftime(a:portfile) !=# old.time
call s:unregister_connection(old.connection)
let old = {}
endif
if empty(old) && getfsize(a:portfile) > 0
let port = matchstr(readfile(a:portfile, 'b', 1)[0], '\d\+')
let s:repl_portfiles[a:portfile] = {'time': getftime(a:portfile)}
try
let conn = fireplace#nrepl_connection#open(port)
let s:repl_portfiles[a:portfile].connection = conn
call s:register_connection(conn, a:0 ? a:1 : '')
return conn
catch /^nREPL Connection Error:/
if &verbose
echohl WarningMSG
echomsg v:exception
echohl None
endif
return {}
endtry
else
return get(old, 'connection', {})
endif
endfunction
" Section: :Connect
command! -bar -complete=customlist,s:connect_complete -nargs=* FireplaceConnect :exe s:Connect()
function! fireplace#input_host_port() abort
let arg = input('Host> ', 'localhost')
if arg ==# ''
return ''
endif
echo "\n"
let arg .= ':' . input('Port> ')
if arg =~# ':$'
return ''
endif
echo "\n"
return arg
endfunction
function! s:protos() abort
return map(split(globpath(&runtimepath, 'autoload/fireplace/*_connection.vim'), "\n"), 'fnamemodify(v:val, ":t")[0:-16]')
endfunction
function! s:connect_complete(A, L, P) abort
let proto = matchstr(a:A, '\w\+\ze://')
if proto ==# ''
let options = map(s:protos(), 'v:val."://"')
else
let rest = matchstr(a:A, '://\zs.*')
try
let options = fireplace#{proto}_connection#complete(rest)
catch /^Vim(let):E117/
let options = ['localhost:']
endtry
call map(options, 'proto."://".v:val')
endif
if a:A !=# ''
call filter(options, 'v:val[0 : strlen(a:A)-1] ==# a:A')
endif
return options
endfunction
function! s:Connect(...) abort
if (a:0 ? a:1 : '') =~# '^\w\+://'
let [proto, arg] = split(a:1, '://')
elseif (a:0 ? a:1 : '') =~# '^\%([[:alnum:].-]\+:\)\=\d\+$'
let [proto, arg] = ['nrepl', a:1]
elseif a:0
return 'echoerr '.string('Usage: :Connect proto://...')
else
let protos = s:protos()
if empty(protos)
return 'echoerr '.string('No protocols available')
endif
let proto = s:inputlist('Protocol> ', protos)
if proto ==# ''
return
endif
redraw!
echo ':Connect'
echo 'Protocol> '.proto
let arg = fireplace#{proto}_connection#prompt()
endif
try
let connection = fireplace#{proto}_connection#open(arg)
catch /.*/
return 'echoerr '.string(v:exception)
endtry
if type(connection) !=# type({}) || empty(connection)
return ''
endif
let client = s:register_connection(connection)
echo 'Connected to '.proto.'://'.arg
let path = fnamemodify(exists('b:java_root') ? b:java_root : fnamemodify(expand('%'), ':p:s?.*\zs[\/]src[\/].*??'), ':~')
let root = a:0 > 1 ? expand(a:2) : input('Scope connection to: ', path, 'dir')
if root !=# '' && root !=# '-'
let s:repl_paths[fnamemodify(root, ':p:s?.\zs[\/]$??')] = client
endif
return ''
endfunction
function! s:piggieback(arg, remove) abort
let response = fireplace#platform().piggieback(a:arg, a:remove)
call s:output_response(response)
endfunction
augroup fireplace_connect
autocmd!
autocmd FileType clojure command! -buffer -bar -complete=customlist,s:connect_complete -nargs=*
\ Connect FireplaceConnect
autocmd FileType clojure command! -buffer -bang -complete=customlist,fireplace#eval_complete -nargs=*
\ Piggieback call s:piggieback(, 0)
augroup END
" Section: Java runner
let s:oneoff_pr = tempname()
let s:oneoff_ex = tempname()
let s:oneoff_stk = tempname()
let s:oneoff_in = tempname()
let s:oneoff_out = tempname()
let s:oneoff_err = tempname()
function! s:spawning_eval(classpath, expr, ns) abort
if a:ns !=# '' && a:ns !=# 'user'
let ns = '(require '.s:qsym(a:ns).') (in-ns '.s:qsym(a:ns).') '
else
let ns = ''
endif
call writefile([], s:oneoff_pr, 'b')
call writefile([], s:oneoff_ex, 'b')
call writefile([], s:oneoff_stk, 'b')
call writefile(split('(do '.a:expr.')', "\n"), s:oneoff_in, 'b')
call writefile([], s:oneoff_out, 'b')
call writefile([], s:oneoff_err, 'b')
let java_cmd = exists('$JAVA_CMD') ? $JAVA_CMD : 'java'
let command = java_cmd.' -cp '.shellescape(a:classpath).' clojure.main -e ' .
\ shellescape(
\ '(binding [*out* (java.io.FileWriter. '.s:str(s:oneoff_out).')' .
\ ' *err* (java.io.FileWriter. '.s:str(s:oneoff_err).')]' .
\ ' (try' .
\ ' (require ''clojure.repl ''clojure.java.javadoc) '.ns.'(spit '.s:str(s:oneoff_pr).' (pr-str (eval (read-string (slurp '.s:str(s:oneoff_in).')))))' .
\ ' (catch Exception e' .
\ ' (spit *err* (.toString e))' .
\ ' (spit '.s:str(s:oneoff_ex).' (class e))' .
\ ' (spit '.s:str(s:oneoff_stk).' (apply str (interpose "\n" (.getStackTrace e))))))' .
\ ' nil)')
let captured = system(command)
let result = {}
let result.value = join(readfile(s:oneoff_pr, 'b'), "\n")
let result.out = join(readfile(s:oneoff_out, 'b'), "\n")
let result.err = join(readfile(s:oneoff_err, 'b'), "\n")
let result.ex = join(readfile(s:oneoff_ex, 'b'), "\n")
let result.stacktrace = readfile(s:oneoff_stk)
if empty(result.ex)
let result.status = ['done']
else
let result.status = ['eval-error', 'done']
endif
call filter(result, '!empty(v:val)')
if v:shell_error && get(result, 'ex', '') ==# ''
throw 'Error running Java: '.get(split(captured, "\n"), -1, '')
else
return result
endif
endfunction
let s:oneoff = {}
function! s:oneoff.user_ns() abort
return 'user'
endfunction
function! s:oneoff.path() dict abort
return self._path
endfunction
function! s:oneoff.eval(expr, options) dict abort
if !empty(get(a:options, 'session', 1))
throw 'Fireplace: no live REPL connection'
endif
let result = s:spawning_eval(join(self.path(), has('win32') ? ';' : ':'),
\ a:expr, get(a:options, 'ns', self.user_ns()))
if has_key(a:options, 'id')
let result.id = a:options.id
endif
return result
endfunction
function! s:oneoff.message(...) abort
throw 'Fireplace: no live REPL connection'
endfunction
let s:oneoff.piggieback = s:oneoff.message
" Section: Client
function! s:buf() abort
if exists('s:input')
return s:input
elseif has_key(s:qffiles, expand('%:p'))
return s:qffiles[expand('%:p')].buffer
else
return '%'
endif
endfunction
function! s:includes_file(file, path) abort
let file = substitute(a:file, '\C^zipfile:\(.*\)::', '\1/', '')
let file = substitute(file, '\C^fugitive:[\/][\/]\(.*\)\.git[\/][\/][^\/]\+[\/]', '\1', '')
for path in a:path
if file[0 : len(path)-1] ==? path
return 1
endif
endfor
endfunction
function! s:path_extract(path)
let path = []
if a:path =~# '\.jar'
for elem in split(substitute(a:path, ',$', '', ''), ',')
if elem ==# ''
let path += ['.']
else
let path += split(glob(substitute(elem, '\\\ze[\\ ,]', '', 'g'), 1), "\n")
endif
endfor
endif
return path
endfunction
function! fireplace#path(...) abort
let buf = a:0 ? a:1 : s:buf()
for repl in s:repls
if s:includes_file(fnamemodify(bufname(buf), ':p'), repl.path())
return repl.path()
endif
endfor
return s:path_extract(getbufvar(buf, '&path'))
endfunction
function! fireplace#platform(...) abort
for [k, v] in items(s:repl_portfiles)
if getftime(k) != v.time
call s:unregister_connection(v.connection)
endif
endfor
let portfile = findfile('.nrepl-port', '.;')
if !empty(portfile)
call fireplace#register_port_file(portfile, fnamemodify(portfile, ':h'))
endif
silent doautocmd User FireplacePreConnect
let buf = a:0 ? a:1 : s:buf()
let root = simplify(fnamemodify(bufname(buf), ':p:s?[\/]$??'))
let previous = ""
while root !=# previous
if has_key(s:repl_paths, root)
return s:repl_paths[root]
endif
let previous = root
let root = fnamemodify(root, ':h')
endwhile
for repl in s:repls
if s:includes_file(fnamemodify(bufname(buf), ':p'), repl.path())
return repl
endif
endfor
let path = s:path_extract(getbufvar(buf, '&path'))
if !empty(path) && fnamemodify(bufname(buf), ':e') =~# '^cljx\=$'
return extend({'_path': path, 'nr': bufnr(buf)}, s:oneoff)
endif
throw 'Fireplace: :Connect to a REPL or install classpath.vim'
endfunction
function! fireplace#client(...) abort
let buf = a:0 ? a:1 : s:buf()
let client = fireplace#platform(buf)
if fnamemodify(bufname(buf), ':e') ==# 'cljs'
if !has_key(client, 'connection')
throw 'Fireplace: no live REPL connection'
endif
if empty(client.piggiebacks)
let result = client.piggieback('')
if has_key(result, 'ex')
return result
endif
endif
return client.piggiebacks[0]
endif
return client
endfunction
function! fireplace#message(payload, ...) abort
let client = fireplace#client()
let payload = copy(a:payload)
if !has_key(payload, 'ns')
let payload.ns = fireplace#ns()
elseif empty(payload.ns)
unlet payload.ns
endif
return call(client.message, [payload] + a:000, client)
endfunction
function! fireplace#op_available(op) abort
try
let client = fireplace#platform()
if has_key(client, 'connection')
return client.connection.has_op(a:op)
endif
catch /^Fireplace: :Connect to a REPL/
endtry
endfunction
function! fireplace#findresource(resource, ...) abort
if a:resource ==# ''
return ''
endif
let resource = a:resource
if a:0 > 2 && type(a:3) == type([])
let suffixes = a:3
else
let suffixes = [''] + split(get(a:000, 2, ''), ',')
endif
for dir in a:0 ? a:1 : fireplace#path()
for suffix in suffixes
if fnamemodify(dir, ':e') ==# 'jar' && index(fireplace#jar_contents(dir), resource . suffix) >= 0
return 'zipfile:' . dir . '::' . resource . suffix
elseif filereadable(dir . '/' . resource . suffix)
return dir . (exists('+shellslash') && !&shellslash ? '\' : '/') . resource . suffix
endif
endfor
endfor
return ''
endfunction
function! s:output_response(response) abort
let substitution_pat = '\e\[[0-9;]*m\|\r\|\n$'
if get(a:response, 'err', '') !=# ''
echohl ErrorMSG
echo substitute(a:response.err, substitution_pat, '', 'g')
echohl NONE
endif
if get(a:response, 'out', '') !=# ''
echo substitute(a:response.out, substitution_pat, '', 'g')
endif
endfunction
function! s:eval(expr, ...) abort
let options = a:0 ? copy(a:1) : {}
let client = fireplace#client()
if !has_key(options, 'ns')
let options.ns = fireplace#ns()
endif
return client.eval(a:expr, options)
endfunction
function! s:temp_response(response) abort
let output = []
if get(a:response, 'err', '') !=# ''
let output = map(split(a:response.err, "\n"), '";!!".v:val')
endif
if get(a:response, 'out', '') !=# ''
let output = map(split(a:response.out, "\n"), '";".v:val')
endif
if has_key(a:response, 'value')
let output += [a:response.value]
endif
let temp = tempname().'.clj'
call writefile(output, temp)
return temp
endfunction
if !exists('s:history')
let s:history = []
endif
if !exists('s:qffiles')
let s:qffiles = {}
endif
function! s:qfentry(entry) abort
if !has_key(a:entry, 'tempfile')
let a:entry.tempfile = s:temp_response(a:entry.response)
endif
let s:qffiles[a:entry.tempfile] = a:entry
return {'filename': a:entry.tempfile, 'text': a:entry.code, 'type': 'E'}
endfunction
function! s:qfhistory() abort
let list = []
for entry in reverse(s:history)
if !has_key(entry, 'tempfile')
let entry.tempfile = s:temp_response(entry.response)
endif
call extend(list, [s:qfentry(entry)])
endfor
return list
endfunction
function! fireplace#session_eval(expr, ...) abort
let response = s:eval(a:expr, a:0 ? a:1 : {})
if !empty(get(response, 'value', '')) || !empty(get(response, 'err', ''))
call insert(s:history, {'buffer': bufnr(''), 'code': a:expr, 'ns': fireplace#ns(), 'response': response})
endif
if len(s:history) > &history
call remove(s:history, &history, -1)
endif
if !empty(get(response, 'stacktrace', []))
let nr = 0
if has_key(s:qffiles, expand('%:p'))
let nr = winbufnr(s:qffiles[expand('%:p')].buffer)
endif
if nr != -1
call setloclist(nr, fireplace#quickfix_for(response.stacktrace))
endif
endif
call s:output_response(response)
if get(response, 'ex', '') !=# ''
let err = 'Clojure: '.response.ex
elseif has_key(response, 'value')
return response.value
else
let err = 'fireplace.vim: Something went wrong: '.string(response)
endif
throw err
endfunction
function! fireplace#eval(...) abort
return call('fireplace#session_eval', a:000)
endfunction
function! fireplace#echo_session_eval(expr, ...) abort
try
echo fireplace#session_eval(a:expr, a:0 ? a:1 : {})
catch /^Clojure:/
catch
echohl ErrorMSG
echomsg v:exception
echohl NONE
endtry
return ''
endfunction
function! fireplace#evalprint(expr) abort
return fireplace#echo_session_eval(a:expr)
endfunction
function! fireplace#macroexpand(fn, form) abort
return fireplace#evalprint('('.a:fn.' (quote '.a:form.'))')
endfunction
let g:fireplace#reader =
\ '(symbol ((fn *vimify [x]' .
\ ' (cond' .
\ ' (map? x) (str "{" (apply str (interpose ", " (map (fn [[k v]] (str (*vimify k) ": " (*vimify v))) x))) "}")' .
\ ' (coll? x) (str "[" (apply str (interpose ", " (map *vimify x))) "]")' .
\ ' (true? x) "1"' .
\ ' (false? x) "0"' .
\ ' (number? x) (pr-str x)' .
\ ' (keyword? x) (pr-str (name x))' .
\ ' :else (pr-str (str x)))) %s))'
function! fireplace#evalparse(expr, ...) abort
let options = extend({'session': 0}, a:0 ? a:1 : {})
let response = s:eval(printf(g:fireplace#reader, a:expr), options)
call s:output_response(response)
if get(response, 'ex', '') !=# ''
let err = 'Clojure: '.response.ex
elseif has_key(response, 'value')
return empty(response.value) ? '' : eval(response.value)
else
let err = 'fireplace.vim: Something went wrong: '.string(response)
endif
throw err
endfunction
" Section: Quickfix
function! s:qfmassage(line, path) abort
let entry = {'text': a:line}
let match = matchlist(a:line, '\(\S\+\)\s\=(\(\S\+\))')
if !empty(match)
let [_, class, file; __] = match
if file =~# '^NO_SOURCE_FILE:' || file !~# ':'
let entry.resource = ''
let entry.lnum = 0
else
let truncated = substitute(class, '\.[A-Za-z0-9_]\+\%([$/].*\)$', '', '')
let entry.resource = tr(truncated, '.', '/').'/'.split(file, ':')[0]
let entry.lnum = split(file, ':')[-1]
endif
let entry.filename = fireplace#findresource(entry.resource, a:path)
if empty(entry.filename)
let entry.lnum = 0
else
let entry.text = class
endif
endif
return entry
endfunction
function! fireplace#quickfix_for(stacktrace) abort
let path = fireplace#path()
return map(copy(a:stacktrace), 's:qfmassage(v:val, path)')
endfunction
function! s:massage_quickfix() abort
let p = substitute(matchstr(','.&errorformat, ',classpath\zs\%(\\.\|[^\,]\)*'), '\\\ze[\,%]', '', 'g')
if empty(p)
return
endif
let path = p[0] ==# ',' ? s:path_extract(p[1:-1]) : split(p[1:-1], p[0])
let qflist = getqflist()
for entry in qflist
call extend(entry, s:qfmassage(get(entry, 'text', ''), path))
endfor
call setqflist(qflist, 'replace')
endfunction
augroup fireplace_quickfix
autocmd!
autocmd QuickFixCmdPost make,cfile,cgetfile call s:massage_quickfix()
augroup END
" Section: Eval
let fireplace#skip = 'synIDattr(synID(line("."),col("."),1),"name") =~? "comment\\|string\\|char"'
function! s:opfunc(type) abort
let sel_save = &selection
let cb_save = &clipboard
let reg_save = @@
try
set selection=inclusive clipboard-=unnamed clipboard-=unnamedplus
if type(a:type) == type(0)
let open = '[[{(]'
let close = '[]})]'
if getline('.')[col('.')-1] =~# close
let [line1, col1] = searchpairpos(open, '', close, 'bn', g:fireplace#skip)
let [line2, col2] = [line('.'), col('.')]
else
let [line1, col1] = searchpairpos(open, '', close, 'bcn', g:fireplace#skip)
let [line2, col2] = searchpairpos(open, '', close, 'n', g:fireplace#skip)
endif
while col1 > 1 && getline(line1)[col1-2] =~# '[#''`~@]'
let col1 -= 1
endwhile
call setpos("'[", [0, line1, col1, 0])
call setpos("']", [0, line2, col2, 0])
silent exe "normal! `[v`]y"
elseif a:type =~# '^.$'
silent exe "normal! `<" . a:type . "`>y"
elseif a:type ==# 'line'
silent exe "normal! '[V']y"
elseif a:type ==# 'block'
silent exe "normal! `[\`]y"
elseif a:type ==# 'outer'
call searchpair('(','',')', 'Wbcr', g:fireplace#skip)
silent exe "normal! vaby"
else
silent exe "normal! `[v`]y"
endif
redraw
return repeat("\n", line("'<")-1) . repeat(" ", col("'<")-1) . @@
finally
let @@ = reg_save
let &selection = sel_save
let &clipboard = cb_save
endtry
endfunction
function! s:filterop(type) abort
let reg_save = @@
let sel_save = &selection
let cb_save = &clipboard
try
set selection=inclusive clipboard-=unnamed clipboard-=unnamedplus
let expr = s:opfunc(a:type)
let @@ = fireplace#session_eval(matchstr(expr, '^\n\+').expr).matchstr(expr, '\n\+$')
if @@ !~# '^\n*$'
normal! gvp
endif
catch /^Clojure:/
return ''
finally
let @@ = reg_save
let &selection = sel_save
let &clipboard = cb_save
endtry
endfunction
function! s:macroexpandop(type) abort
call fireplace#macroexpand("clojure.walk/macroexpand-all", s:opfunc(a:type))
endfunction
function! s:macroexpand1op(type) abort
call fireplace#macroexpand("macroexpand-1", s:opfunc(a:type))
endfunction
function! s:printop(type) abort
let s:todo = s:opfunc(a:type)
call feedkeys("\FireplacePrintLast")
endfunction
function! s:print_last() abort
call fireplace#echo_session_eval(s:todo, {'file_path': s:buffer_path()})
return ''
endfunction
function! s:editop(type) abort
call feedkeys(eval('"\'.&cedit.'"') . "\", 'n')
let input = s:input(substitute(substitute(substitute(
\ s:opfunc(a:type), "\s*;[^\n\"]*\\%(\n\\@=\\|$\\)", '', 'g'),
\ '\n\+\s*', ' ', 'g'),
\ '^\s*', '', ''))
if input !=# ''
call fireplace#echo_session_eval(input)
endif
endfunction
function! s:Eval(bang, line1, line2, count, args) abort
let options = {}
if a:args !=# ''
let expr = a:args
else
if a:count ==# 0
let open = '[[{(]'
let close = '[]})]'
let [line1, col1] = searchpairpos(open, '', close, 'bcrn', g:fireplace#skip)
let [line2, col2] = searchpairpos(open, '', close, 'rn', g:fireplace#skip)
if !line1 && !line2
let [line1, col1] = searchpairpos(open, '', close, 'brn', g:fireplace#skip)
let [line2, col2] = searchpairpos(open, '', close, 'crn', g:fireplace#skip)
endif
while col1 > 1 && getline(line1)[col1-2] =~# '[#''`~@]'
let col1 -= 1
endwhile
else
let line1 = a:line1
let line2 = a:line2
let col1 = 1
let col2 = strlen(getline(line2))
endif
if !line1 || !line2
return ''
endif
let options.file_path = s:buffer_path()
let expr = repeat("\n", line1-1).repeat(" ", col1-1)
if line1 == line2
let expr .= getline(line1)[col1-1 : col2-1]
else
let expr .= getline(line1)[col1-1 : -1] . "\n"
\ . join(map(getline(line1+1, line2-1), 'v:val . "\n"'))
\ . getline(line2)[0 : col2-1]
endif
if a:bang
exe line1.','.line2.'delete _'
endif
endif
if a:bang
try
let result = fireplace#session_eval(expr, options)
if a:args !=# ''
call append(a:line1, result)
exe a:line1
else
call append(a:line1-1, result)
exe a:line1-1
endif
catch /^Clojure:/
endtry
else
call fireplace#echo_session_eval(expr, options)
endif
return ''
endfunction
" If we call input() directly inside a try, and the user opens the command
" line window and tries to switch out of it (such as with ctrl-w), Vim will
" crash when the command line window closes. Adding an indirect function call
" works around this.
function! s:actually_input(...) abort
return call(function('input'), a:000)
endfunction
function! s:input(default) abort
if !exists('g:FIREPLACE_HISTORY') || type(g:FIREPLACE_HISTORY) != type([])
unlet! g:FIREPLACE_HISTORY
let g:FIREPLACE_HISTORY = []
endif
try
let s:input = bufnr('%')
let s:oldhist = s:histswap(g:FIREPLACE_HISTORY)
return s:actually_input(fireplace#ns().'=> ', a:default, 'customlist,fireplace#eval_complete')
finally
unlet! s:input
if exists('s:oldhist')
let g:FIREPLACE_HISTORY = s:histswap(s:oldhist)
endif
endtry
endfunction
function! s:inputclose() abort
let l = substitute(getcmdline(), '"\%(\\.\|[^"]\)*"\|\\.', '', 'g')
let open = len(substitute(l, '[^(]', '', 'g'))
let close = len(substitute(l, '[^)]', '', 'g'))
if open - close == 1
return ")\"
else
return ")"
endif
endfunction
function! s:inputeval() abort
let input = s:input('')
redraw
if input !=# ''
call fireplace#echo_session_eval(input)
endif
return ''
endfunction
function! s:recall() abort
try
cnoremap ) inputclose()
let input = s:input('(')
if input =~# '^(\=$'
return ''
else
return fireplace#session_eval(input)
endif
catch /^Clojure:/
return ''
finally
silent! cunmap )
endtry
endfunction
function! s:histswap(list) abort
let old = []
for i in range(1, histnr('@') * (histnr('@') > 0))
call extend(old, [histget('@', i)])
endfor
call histdel('@')
for entry in a:list
call histadd('@', entry)
endfor
return old
endfunction
nnoremap FireplacePrintLast :exe print_last()
nnoremap FireplacePrint :set opfunc=printopg@
xnoremap FireplacePrint :call printop(visualmode())
nnoremap FireplaceCountPrint :call printop(v:count)
nnoremap FireplaceFilter :set opfunc=filteropg@
xnoremap FireplaceFilter :call filterop(visualmode())
nnoremap FireplaceCountFilter :call filterop(v:count)
nnoremap FireplaceMacroExpand :set opfunc=macroexpandopg@
xnoremap FireplaceMacroExpand :call macroexpandop(visualmode())
nnoremap FireplaceCountMacroExpand :call macroexpandop(v:count)
nnoremap Fireplace1MacroExpand :set opfunc=macroexpand1opg@
xnoremap Fireplace1MacroExpand :call macroexpand1op(visualmode())
nnoremap Fireplace1MacroExpand :call macroexpand1op(v:count)
nnoremap FireplaceEdit :set opfunc=editopg@
xnoremap FireplaceEdit :call editop(visualmode())
nnoremap FireplaceCountEdit :call editop(v:count)
nnoremap FireplacePrompt :exe inputeval()
noremap! FireplaceRecall =recall()
function! s:Last(bang, count) abort
if len(s:history) < a:count
return 'echoerr "History entry not found"'
endif
let history = s:qfhistory()
let last = s:qfhistory()[a:count-1]
execute 'pedit '.last.filename
if !&previewwindow
let nr = winnr()
wincmd p
wincmd P
endif
call setloclist(0, history)
silent exe 'llast '.(len(history)-a:count+1)
if exists('nr') && a:bang
wincmd p
exe nr.'wincmd w'
endif
return ''
endfunction
function! s:set_up_eval() abort
command! -buffer -bang -range=0 -nargs=? -complete=customlist,fireplace#eval_complete Eval :exe s:Eval(0, , , , )
command! -buffer -bang -bar -count=1 Last exe s:Last(0, )
nmap cp FireplacePrint
nmap cpp FireplaceCountPrint
nmap c! FireplaceFilter
nmap c!! FireplaceCountFilter
nmap cm FireplaceMacroExpand
nmap cmm FireplaceCountMacroExpand
nmap c1m Fireplace1MacroExpand
nmap c1mm FireplaceCount1MacroExpand
nmap cq FireplaceEdit
nmap cqq FireplaceCountEdit
nmap cqp FireplacePrompt
exe 'nmap cqc FireplacePrompt' . &cedit . 'i'
map! ( FireplaceRecall
endfunction
function! s:set_up_historical() abort
setlocal readonly nomodifiable
nnoremap q :bdelete
endfunction
function! s:cmdwinenter() abort
setlocal filetype=clojure
endfunction
function! s:cmdwinleave() abort
setlocal filetype< omnifunc<
endfunction
augroup fireplace_eval
autocmd!
autocmd FileType clojure call s:set_up_eval()
autocmd BufReadPost * if has_key(s:qffiles, expand(':p')) |
\ call s:set_up_historical() |
\ endif
autocmd CmdWinEnter @ if exists('s:input') | call s:cmdwinenter() | endif
autocmd CmdWinLeave @ if exists('s:input') | call s:cmdwinleave() | endif
augroup END
" Section: :Require
function! s:Require(bang, echo, ns) abort
if &autowrite || &autowriteall
silent! wall
endif
if expand('%:e') ==# 'cljs'
let cmd = '(load-file '.s:str(tr(a:ns ==# '' ? fireplace#ns() : a:ns, '-.', '_/').'.cljs').')'
else
let cmd = ('(require '.s:qsym(a:ns ==# '' ? fireplace#ns() : a:ns).' :reload'.(a:bang ? '-all' : '').')')
endif
if a:echo
echo cmd
endif
try
call fireplace#session_eval(cmd, {'ns': fireplace#client().user_ns()})
return ''
catch /^Clojure:.*/
return ''
endtry
endfunction
function! s:set_up_require() abort
command! -buffer -bar -bang -complete=customlist,fireplace#ns_complete -nargs=? Require :exe s:Require(0, 1, )
nnoremap cpr :if expand('%:e') ==# 'cljs'RequireelseRunTestsendif
endfunction
augroup fireplace_require
autocmd!
autocmd FileType clojure call s:set_up_require()
augroup END
" Section: Go to source
function! fireplace#info(symbol) abort
if fireplace#op_available('info')
let response = fireplace#message({'op': 'info', 'symbol': a:symbol})[0]
if type(get(response, 'value')) == type({})
return response.value
endif
endif
let cmd =
\ '(if-let [m (meta (resolve ' . s:qsym(a:symbol) .'))]'
\ . ' {:name (:name m)'
\ . ' :ns (:ns m)'
\ . ' :resource (:file m)'
\ . ' :line (:line m)'
\ . ' :doc (:doc m)'
\ . ' :arglists-str (str (:arglists m))}'
\ . ' {})'
return fireplace#evalparse(cmd)
endfunction
function! fireplace#source(symbol) abort
let info = fireplace#info(a:symbol)
let file = ''
if !empty(get(info, 'resource'))
let file = fireplace#findresource(info.resource)
elseif get(info, 'file') =~# '^/\|^\w:\\' && filereadable(info.file)
let file = info.file
endif
if !empty(file) && !empty(get(info, 'line'))
return '+' . info.line . ' ' . fnameescape(file)
endif
return ''
endfunction
function! s:Edit(cmd, keyword) abort
try
if a:keyword =~# '^\k\+[/.]$'
let location = fireplace#findfile(a:keyword[0: -2])
elseif a:keyword =~# '^\k\+\.[^/.]\+$'
let location = fireplace#findfile(a:keyword)
else
let location = fireplace#source(a:keyword)
endif
catch /^Clojure:/
return ''
endtry
if location !=# ''
if matchstr(location, '^+\d\+ \zs.*') ==# fnameescape(expand('%:p')) && a:cmd ==# 'edit'
normal! m'
return matchstr(location, '\d\+')
else
return a:cmd.' '.location.'|let &l:path = '.string(&l:path)
endif
endif
let v:errmsg = "Couldn't find source for ".a:keyword
return 'echoerr v:errmsg'
endfunction
nnoremap FireplaceDjump :exe Edit('edit', expand(''))
nnoremap FireplaceDsplit :exe Edit('split', expand(''))
nnoremap FireplaceDtabjump :exe Edit('tabedit', expand(''))
function! s:set_up_source() abort
setlocal define=^\\s*(def\\w*
command! -bar -buffer -nargs=1 -complete=customlist,fireplace#eval_complete Djump :exe s:Edit('edit', )
command! -bar -buffer -nargs=1 -complete=customlist,fireplace#eval_complete Dsplit :exe s:Edit('split', )
nmap [ FireplaceDjump
nmap ] FireplaceDjump
nmap FireplaceDsplit
nmap d FireplaceDsplit
nmap gd FireplaceDtabjump
endfunction
augroup fireplace_source
autocmd!
autocmd FileType clojure call s:set_up_source()
augroup END
" Section: Go to file
function! fireplace#findfile(path) abort
let path = a:path
if a:path !~# '/'
let path = tr(a:path, '.-', '/_')
else
let path = substitute(a:path, '^/', '', '')
endif
let resource = fireplace#findresource(path, fireplace#path(), 0, &suffixesadd)
if !empty(resource)
return resource
elseif fnamemodify(a:path, ':p') ==# a:path && filereadable(a:path)
return path
elseif a:path[0] !=# '/' && filereadable(expand('%:h') . '/' . path)
return expand('%:h') . '/' . path
endif
return ''
endfunction
function! s:GF(cmd, file) abort
if a:file =~# '^[^/]*/[^/.]*$' && a:file =~# '^\k\+$'
let [file, jump] = split(a:file, "/")
if file !~# '\.'
try
let file = fireplace#evalparse('((ns-aliases *ns*) '.s:qsym(file).' '.s:qsym(file).')')
catch /^Clojure:/
endtry
endif
else
let file = a:file
endif
let file = fireplace#findfile(file)
if file ==# ''
let v:errmsg = "Couldn't find file for ".a:file
return 'echoerr v:errmsg'
endif
return a:cmd .
\ (exists('jump') ? ' +sil!\ djump\ ' . jump : '') .
\ ' ' . fnameescape(file) .
\ '| let &l:path = ' . string(&l:path)
endfunction
nnoremap FireplaceEditFile :exe GF('edit', expand(''))
nnoremap FireplaceSplitFile :exe GF('split', expand(''))
nnoremap FireplaceTabeditFile :exe GF('tabedit', expand(''))
function! s:set_up_go_to_file() abort
setlocal includeexpr=fireplace#findfile(tr(substitute(v:fname,'/[^/.]*$','',''),'.-','/_'))
if expand('%:e') ==# 'cljs'
setlocal suffixesadd=.cljs,.cljx,.clj,.java
else
setlocal suffixesadd=.clj,.cljx,.cljs,.java
endif
nmap gf FireplaceEditFile
nmap f FireplaceSplitFile
nmap FireplaceSplitFile
nmap gf FireplaceTabeditFile
endfunction
augroup fireplace_go_to_file
autocmd!
autocmd FileType clojure call s:set_up_go_to_file()
augroup END
" Section: Documentation
function! s:buffer_path(...) abort
let buffer = a:0 ? a:1 : s:buf()
if getbufvar(buffer, '&buftype') =~# '^no'
return ''
endif
let path = substitute(fnamemodify(bufname(buffer), ':p'), '\C^zipfile:\(.*\)::', '\1/', '')
for dir in fireplace#path(buffer)
if dir !=# '' && path[0 : strlen(dir)-1] ==# dir && path[strlen(dir)] =~# '[\/]'
return path[strlen(dir)+1:-1]
endif
endfor
return ''
endfunction
function! fireplace#ns(...) abort
let buffer = a:0 ? a:1 : s:buf()
if !empty(getbufvar(buffer, 'fireplace_ns'))
return getbufvar(buffer, 'fireplace_ns')
endif
let head = getbufline(buffer, 1, 500)
let blank = '^\s*\%(;.*\)\=$'
call filter(head, 'v:val !~# blank')
let keyword_group = '[A-Za-z0-9_?*!+/=<>.-]'
let lines = join(head[0:49], ' ')
let lines = substitute(lines, '"\%(\\.\|[^"]\)*"\|\\.', '', 'g')
let lines = substitute(lines, '\^\={[^{}]*}', '', '')
let lines = substitute(lines, '\^:'.keyword_group.'\+', '', 'g')
let ns = matchstr(lines, '\C^(\s*\%(in-ns\s*''\|ns\s\+\)\zs'.keyword_group.'\+\ze')
if ns !=# ''
return ns
endif
let path = s:buffer_path(buffer)
return s:to_ns(path ==# '' ? fireplace#client(buffer).user_ns() : path)
endfunction
function! s:Lookup(ns, macro, arg) abort
try
let response = s:eval('('.a:ns.'/'.a:macro.' '.a:arg.')', {'session': 0})
call s:output_response(response)
catch /^Clojure:/
catch /.*/
echohl ErrorMSG
echo v:exception
echohl None
endtry
return ''
endfunction
function! s:inputlist(label, entries) abort
let choices = [a:label]
for i in range(len(a:entries))
let choices += [printf('%2d. %s', i+1, a:entries[i])]
endfor
let choice = inputlist(choices)
if choice
return a:entries[choice-1]
else
return ''
endif
endfunction
function! s:Doc(symbol) abort
let info = fireplace#info(a:symbol)
if has_key(info, 'ns') && has_key(info, 'name')
echo info.ns . '/' . info.name
endif
if get(info, 'arglists-str', 'nil') !=# 'nil'
echo info['arglists-str']
endif
if !empty(get(info, 'doc', ''))
echo ' ' . info.doc
endif
return ''
endfunction
function! s:K() abort
let word = expand('')
let java_candidate = matchstr(word, '^\%(\w\+\.\)*\u\l\w*\ze\%(\.\|\/\w\+\)\=$')
if java_candidate !=# ''
return 'Javadoc '.java_candidate
else
return 'Doc '.word
endif
endfunction
nnoremap FireplaceK :=K()
nnoremap FireplaceSource :Source
function! s:set_up_doc() abort
command! -buffer -nargs=1 FindDoc :exe s:Lookup('clojure.repl', 'find-doc', printf('#"%s"', ))
command! -buffer -bar -nargs=1 Javadoc :exe s:Lookup('clojure.java.javadoc', 'javadoc', )
command! -buffer -bar -nargs=1 -complete=customlist,fireplace#eval_complete Doc :exe s:Doc()
command! -buffer -bar -nargs=1 -complete=customlist,fireplace#eval_complete Source :exe s:Lookup('clojure.repl', 'source', )
setlocal keywordprg=:Doc
if empty(mapcheck('K', 'n'))
nmap K FireplaceK
endif
nmap [d FireplaceSource
nmap ]d FireplaceSource
endfunction
augroup fireplace_doc
autocmd!
autocmd FileType clojure call s:set_up_doc()
augroup END
" Section: Tests
function! fireplace#capture_test_run(expr, ...) abort
let expr = '(require ''clojure.test) '
\ . '(try '
\ . '(binding [clojure.test/report (fn [m]'
\ . ' (case (:type m)'
\ . ' (:fail :error)'
\ . ' (let [{file :file line :line test :name} (meta (last clojure.test/*testing-vars*))]'
\ . ' (clojure.test/with-test-out'
\ . ' (println (clojure.string/join "\t" [file line (name (:type m)) test]))'
\ . ' (when (seq clojure.test/*testing-contexts*) (println (clojure.test/testing-contexts-str)))'
\ . ' (when-let [message (:message m)] (println message))'
\ . ' (println "expected:" (pr-str (:expected m)))'
\ . ' (println " actual:" (pr-str (:actual m)))))'
\ . ' ((.getRawRoot #''clojure.test/report) m)))]'
\ . ' ' . (a:0 ? a:1 : '') . a:expr . ')'
\ . ' (catch Exception e'
\ . ' (println (str e))'
\ . ' (println (clojure.string/join "\n" (.getStackTrace e)))))'
let qflist = []
let response = s:eval(expr, {'session': 0})
if !has_key(response, 'out')
call setqflist(fireplace#quickfix_for(get(response, 'stacktrace', [])))
return s:output_response(response)
endif
for line in split(response.out, "\n")
if line =~# '\t.*\t.*\t'
let entry = {'text': line}
let [resource, lnum, type, name] = split(line, "\t", 1)
let entry.lnum = lnum
let entry.type = (type ==# 'fail' ? 'W' : 'E')
let entry.text = name
if resource ==# 'NO_SOURCE_FILE'
let resource = ''
let entry.lnum = 0
endif
let entry.filename = fireplace#findresource(resource, fireplace#path())
if empty(entry.filename)
let entry.lnum = 0
endif
else
let entry = s:qfmassage(line, fireplace#path())
endif
call add(qflist, entry)
endfor
call setqflist(qflist)
let was_qf = &buftype ==# 'quickfix'
botright cwindow
if &buftype ==# 'quickfix' && !was_qf
wincmd p
endif
for winnr in range(1, winnr('$'))
if getwinvar(winnr, '&buftype') ==# 'quickfix'
call setwinvar(winnr, 'quickfix_title', a:expr)
return
endif
endfor
endfunction
function! s:RunTests(bang, ...) abort
if &autowrite || &autowriteall
silent! wall
endif
let reqs = map(copy(a:000), '"''".v:val')
let pre = '(clojure.core/require '.join(empty(a:000) ? ["'".fireplace#ns()] : reqs, ' ').' :reload) '
let expr = join(['(clojure.test/run-tests'] + reqs, ' ').')'
call fireplace#capture_test_run(expr, pre)
echo expr
endfunction
function! s:RunAllTests(bang, ...) abort
if a:0
let expr = '(clojure.test/run-all-tests #"'.join(a:000, '|').'")'
else
let expr = '(clojure.test/run-all-tests)'
endif
call fireplace#capture_test_run(expr)
echo expr
endfunction
function! s:set_up_tests() abort
command! -buffer -bar -bang -nargs=*
\ -complete=customlist,fireplace#ns_complete RunTests
\ call s:RunTests(0, )
command! -buffer -bang -nargs=* RunAllTests
\ call s:RunAllTests(0, )
endfunction
augroup fireplace_tests
autocmd!
autocmd FileType clojure call s:set_up_tests()
augroup END