981 lines
28 KiB
VimL
981 lines
28 KiB
VimL
" foreplay.vim - Clojure REPL tease
|
|
" Maintainer: Tim Pope <http://tpo.pe>
|
|
|
|
if exists("g:loaded_foreplay") || v:version < 700 || &cp
|
|
finish
|
|
endif
|
|
let g:loaded_foreplay = 1
|
|
|
|
" File type {{{1
|
|
|
|
augroup foreplay_file_type
|
|
autocmd!
|
|
autocmd BufNewFile,BufReadPost *.clj setfiletype clojure
|
|
autocmd FileType clojure
|
|
\ if expand('%:p') !~# '^zipfile:' |
|
|
\ let &l:path = classpath#detect() |
|
|
\ endif
|
|
augroup END
|
|
|
|
" }}}1
|
|
" Shell escaping {{{1
|
|
|
|
function! foreplay#shellesc(arg) abort
|
|
if a:arg =~ '^[A-Za-z0-9_/.-]\+$'
|
|
return a:arg
|
|
elseif &shell =~# 'cmd'
|
|
return '"'.substitute(substitute(a:arg, '"', '""', 'g'), '%', '"%"', 'g').'"'
|
|
else
|
|
let escaped = shellescape(a:arg)
|
|
if &shell =~# 'sh' && &shell !~# 'csh'
|
|
return substitute(escaped, '\\\n', '\n', 'g')
|
|
else
|
|
return escaped
|
|
endif
|
|
endif
|
|
endfunction
|
|
|
|
" }}}1
|
|
" Completion {{{1
|
|
|
|
let s:jar_contents = {}
|
|
|
|
function! foreplay#jar_contents(path) abort
|
|
if !has_key(s:jar_contents, a:path) && executable('zipinfo')
|
|
let s:jar_contents[a:path] = split(system('zipinfo -1 '.shellescape(a:path)), "\n")
|
|
if v:shell_error
|
|
return []
|
|
endif
|
|
endif
|
|
return copy(get(s:jar_contents, a:path, []))
|
|
endfunction
|
|
|
|
function! foreplay#eval_complete(A, L, P) abort
|
|
let prefix = matchstr(a:A, '\%(.* \|^\)\%(#\=[\[{('']\)*')
|
|
let keyword = a:A[strlen(prefix) : -1]
|
|
return sort(map(foreplay#omnicomplete(0, keyword), 'prefix . v:val.word'))
|
|
endfunction
|
|
|
|
function! foreplay#ns_complete(A, L, P) abort
|
|
let matches = []
|
|
for dir in classpath#split(classpath#from_vim(&path))
|
|
if dir =~# '\.jar$'
|
|
let files = filter(foreplay#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:tons(v:val)'), 'a:A ==# "" || a:A ==# v:val[0 : strlen(a:A)-1]')
|
|
endfunction
|
|
|
|
" This turns a string (potentially with '.' separators into a fuzzy-matching
|
|
" regex. This is where fuzzy matching behavior is defined.
|
|
function! foreplay#fuzzy(base)
|
|
let not_dot = '[^.]*'
|
|
let regex = ''
|
|
let anchor = 1 " means that the next char must be in the next position
|
|
for i in range(strlen(a:base))
|
|
let c = a:base[i]
|
|
if c ==# '.'
|
|
let regex .= not_dot.'\.'
|
|
let anchor = 1
|
|
elseif anchor
|
|
let regex .= c
|
|
let anchor = 0
|
|
else
|
|
let regex .= not_dot.c
|
|
endif
|
|
endfor
|
|
return '^'.regex.not_dot.'$'
|
|
endfunction
|
|
|
|
function! foreplay#omnicomplete(findstart, base) abort
|
|
if a:findstart
|
|
let line = getline('.')[0 : col('.')-2]
|
|
return col('.') - strlen(matchstr(line, '\k\+$')) - 1
|
|
else
|
|
try
|
|
let omnifier = '(fn [[k v]] (let [m (meta v)]' .
|
|
\ ' {:word k :menu (pr-str (:arglists m (symbol ""))) :info (str " " (:doc m)) :kind (if (:arglists m) "f" "v")}))'
|
|
|
|
let ns = foreplay#ns()
|
|
|
|
let [aliases, namespaces, maps] = foreplay#evalparse(
|
|
\ '[(ns-aliases '.s:qsym(ns).') (all-ns) '.
|
|
\ '(sort-by :word (map '.omnifier.' (ns-map '.s:qsym(ns).')))]')
|
|
|
|
if a:base =~# '^[^/]*/[^/]*$'
|
|
" This branch generates completions when there's a qualified var
|
|
let ns_fuzzy = foreplay#fuzzy(matchstr(a:base, '^.*\ze/'))
|
|
let name_fuzzy = foreplay#fuzzy(matchstr(a:base, '/\zs.*$'))
|
|
let results = []
|
|
for ns in filter(namespaces + keys(aliases), 'v:val =~# ns_fuzzy')
|
|
let lookup_ns = ns
|
|
if has_key(aliases, ns) "if alias, must use actual ns for lookup
|
|
let lookup_ns = get(aliases, ns)
|
|
endif
|
|
let results = results + foreplay#evalparse(
|
|
\ '(->> (ns-publics '.s:qsym(lookup_ns).')' .
|
|
\ '(filter #(re-matches #"'.name_fuzzy.'" (name (key %))))' .
|
|
\ '(map (fn [[k v]] [(str "'.ns.'" \/ (name k)) v]))' .
|
|
\ '(map '.omnifier.')' .
|
|
\ '(sort-by :word))')
|
|
endfor
|
|
else
|
|
" This branch handles completions for namespaces, aliases, and vars
|
|
let fuzzy_base = foreplay#fuzzy(a:base)
|
|
let results = maps + map(sort(keys(aliases) + namespaces),
|
|
\ '{"word": v:val, "kind": "t", "info": ""}')
|
|
let results = filter(results,
|
|
\ 'a:base ==# "" || v:val.word =~# fuzzy_base')
|
|
endif
|
|
if type(results) == type([])
|
|
return results
|
|
else
|
|
return []
|
|
endif
|
|
catch /.*/
|
|
return []
|
|
endtry
|
|
endif
|
|
endfunction
|
|
|
|
augroup foreplay_completion
|
|
autocmd!
|
|
autocmd FileType clojure setlocal omnifunc=foreplay#omnicomplete
|
|
augroup END
|
|
|
|
" }}}1
|
|
" REPL client {{{1
|
|
|
|
let s:repl = {"requires": {}}
|
|
|
|
if !exists('s:repls')
|
|
let s:repls = []
|
|
let s:repl_paths = {}
|
|
endif
|
|
|
|
function! s:qsym(symbol)
|
|
if a:symbol =~# '^[[:alnum:]?*!+/=<>.:-]\+$'
|
|
return "'".a:symbol
|
|
else
|
|
return '(symbol "'.escape(a:symbol, '"').'")'
|
|
endif
|
|
endfunction
|
|
|
|
function! s:repl.eval(expr, ns) dict abort
|
|
try
|
|
let result = self.connection.eval(a:expr, a:ns)
|
|
catch /^\w\+: Connection/
|
|
call filter(s:repl_paths, 'v:val isnot self')
|
|
call filter(s:repls, 'v:val isnot self')
|
|
throw v:exception
|
|
endtry
|
|
return result
|
|
endfunction
|
|
|
|
function! s:repl.require(lib) dict abort
|
|
if a:lib !~# '^\%(user\)\=$' && !get(self.requires, a:lib, 0)
|
|
let reload = has_key(self.requires, a:lib) ? ' :reload' : ''
|
|
let self.requires[a:lib] = 0
|
|
let result = self.eval('(doto '.s:qsym(a:lib).' (require'.reload.') the-ns)', 'user')
|
|
let self.requires[a:lib] = !has_key(result, 'ex')
|
|
endif
|
|
return ''
|
|
endfunction
|
|
|
|
function! s:repl.includes_file(file) dict abort
|
|
let file = substitute(a:file, '\C^zipfile:\(.*\)::', '\1/', '')
|
|
for path in self.connection.path()
|
|
if file[0 : len(path)-1] ==? path
|
|
return 1
|
|
endif
|
|
endfor
|
|
endfunction
|
|
|
|
function! s:register_connection(conn, ...)
|
|
call insert(s:repls, extend({'connection': a:conn}, deepcopy(s:repl)))
|
|
if a:0 && a:1 !=# ''
|
|
let s:repl_paths[a:1] = s:repls[0]
|
|
endif
|
|
return s:repls[0]
|
|
endfunction
|
|
|
|
" }}}1
|
|
" :Connect {{{1
|
|
|
|
command! -bar -complete=customlist,s:connect_complete -nargs=? ForeplayConnect :exe s:Connect(<q-args>)
|
|
|
|
function! foreplay#input_host_port()
|
|
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()
|
|
return map(split(globpath(&runtimepath, 'autoload/*/foreplay_connection.vim'), "\n"), 'fnamemodify(v:val, ":h:t")')
|
|
endfunction
|
|
|
|
function! s:connect_complete(A, L, P)
|
|
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 = {proto}#foreplay_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(arg)
|
|
if a:arg =~# '^\w\+://'
|
|
let [proto, arg] = split(a:arg, '://')
|
|
elseif a:arg !=# ''
|
|
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 = {proto}#foreplay_connection#prompt()
|
|
endif
|
|
try
|
|
let connection = {proto}#foreplay_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 = input('Scope connection to: ', path, 'dir')
|
|
if root !=# ''
|
|
let s:repl_paths[fnamemodify(root, ':p:s?[\/]$??')] = client
|
|
endif
|
|
return ''
|
|
endfunction
|
|
|
|
augroup foreplay_connect
|
|
autocmd!
|
|
autocmd FileType clojure command! -bar -complete=customlist,s:connect_complete -nargs=? Connect :ForeplayConnect <args>
|
|
augroup END
|
|
|
|
" }}}1
|
|
" Java runner {{{1
|
|
|
|
if !exists('g:java_cmd')
|
|
let g:java_cmd = exists('$JAVA_CMD') ? $JAVA_CMD : 'java'
|
|
endif
|
|
|
|
let s:oneoff = {}
|
|
|
|
let s:oneoff_pr = tempname()
|
|
let s:oneoff_ex = tempname()
|
|
let s:oneoff_in = tempname()
|
|
let s:oneoff_out = tempname()
|
|
let s:oneoff_err = tempname()
|
|
|
|
function! s:oneoff.eval(expr, ns) dict abort
|
|
if &verbose
|
|
echohl WarningMSG
|
|
echomsg "No REPL found. Running java clojure.main ..."
|
|
echohl None
|
|
endif
|
|
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(split('(do '.a:expr.')', "\n"), s:oneoff_in, 'b')
|
|
call writefile([], s:oneoff_out, 'b')
|
|
call writefile([], s:oneoff_err, 'b')
|
|
let command = g:java_cmd.' -cp '.shellescape(self.classpath).' clojure.main -e ' .
|
|
\ foreplay#shellesc(
|
|
\ '(binding [*out* (java.io.FileWriter. "'.s:oneoff_out.'")' .
|
|
\ ' *err* (java.io.FileWriter. "'.s:oneoff_err.'")]' .
|
|
\ ' (try' .
|
|
\ ' (require ''clojure.repl) '.ns.'(spit "'.s:oneoff_pr.'" (pr-str (eval (read-string (slurp "'.s:oneoff_in.'")))))' .
|
|
\ ' (catch Exception e' .
|
|
\ ' (spit *err* (.toString e))' .
|
|
\ ' (spit "'.s:oneoff_ex.'" (class e))))' .
|
|
\ ' nil)')
|
|
let wtf = 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_err, 'b'), "\n")
|
|
call filter(result, 'v:val !=# ""')
|
|
if v:shell_error && result.ex ==# ''
|
|
throw 'Error running Clojure: '.wtf
|
|
else
|
|
return result
|
|
endif
|
|
endfunction
|
|
|
|
function! s:oneoff.require(symbol)
|
|
return ''
|
|
endfunction
|
|
|
|
" }}}1
|
|
" Client {{{1
|
|
|
|
function! s:client() abort
|
|
silent doautocmd User ForeplayPreConnect
|
|
let buf = exists('s:input') ? s:input : '%'
|
|
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
|
|
return foreplay#local_client(1)
|
|
endfunction
|
|
|
|
function! foreplay#client() abort
|
|
return s:client()
|
|
endfunction
|
|
|
|
function! foreplay#local_client(...)
|
|
if !a:0
|
|
silent doautocmd User ForeplayPreConnect
|
|
endif
|
|
let buf = exists('s:input') ? s:input : '%'
|
|
for repl in s:repls
|
|
if repl.includes_file(fnamemodify(bufname(buf), ':p'))
|
|
return repl
|
|
endif
|
|
endfor
|
|
let cp = classpath#from_vim(getbufvar(buf, '&path'))
|
|
return extend({'classpath': cp}, s:oneoff)
|
|
endfunction
|
|
|
|
function! foreplay#eval(expr, ...) abort
|
|
let c = s:client()
|
|
|
|
if !a:0 && foreplay#ns() !~# '^\%(user\)$'
|
|
call c.require(foreplay#ns())
|
|
endif
|
|
|
|
let result = c.eval(a:expr, a:0 ? a:1 : foreplay#ns())
|
|
|
|
if get(result, 'err', '') !=# ''
|
|
echohl ErrorMSG
|
|
echo substitute(result.err, '\n$', '', '')
|
|
echohl NONE
|
|
endif
|
|
if get(result, 'out', '') !=# ''
|
|
echo substitute(result.out, '\n$', '', '')
|
|
endif
|
|
|
|
if get(result, 'ex', '') !=# ''
|
|
let err = 'Clojure: '.result.ex
|
|
elseif has_key(result, 'value')
|
|
return result.value
|
|
else
|
|
let err = 'foreplay.vim: Something went wrong: '.string(result)
|
|
endif
|
|
throw err
|
|
endfunction
|
|
|
|
function! foreplay#evalparse(expr) abort
|
|
let body = foreplay#eval(
|
|
\ '(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))) "]")' .
|
|
\ ' (number? x) (pr-str x)' .
|
|
\ ' (keyword? x) (pr-str (name x))' .
|
|
\ ' :else (pr-str (str x)))) '.a:expr.'))',
|
|
\ a:0 ? a:1 : foreplay#ns())
|
|
if body ==# ''
|
|
return ''
|
|
else
|
|
return eval(body)
|
|
endif
|
|
endfunction
|
|
|
|
" }}}1
|
|
" Eval {{{1
|
|
|
|
let foreplay#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 a:type =~ '^\d\+$'
|
|
silent exe 'normal! ^v'.a:type.'$hy'
|
|
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! `[\<C-V>`]y"
|
|
elseif a:type ==# 'outer'
|
|
call searchpair('(','',')', 'Wbcr', g:foreplay#skip)
|
|
silent exe "normal! vaby"
|
|
else
|
|
silent exe "normal! `[v`]y"
|
|
endif
|
|
redraw
|
|
return @@
|
|
finally
|
|
let @@ = reg_save
|
|
let &selection = sel_save
|
|
let &clipboard = cb_save
|
|
endtry
|
|
endfunction
|
|
|
|
function! s:filterop(type) abort
|
|
let reg_save = @@
|
|
try
|
|
let expr = s:opfunc(a:type)
|
|
let @@ = matchstr(expr, '^\n\+').foreplay#eval(expr, foreplay#ns()).matchstr(expr, '\n\+$')
|
|
if @@ !~# '^\n*$'
|
|
normal! gvp
|
|
endif
|
|
catch /^Clojure:/
|
|
return ''
|
|
finally
|
|
let @@ = reg_save
|
|
endtry
|
|
endfunction
|
|
|
|
function! s:printop(type) abort
|
|
let s:todo = s:opfunc(a:type)
|
|
call feedkeys("\<Plug>ForeplayPrintLast")
|
|
endfunction
|
|
|
|
function! s:print_last() abort
|
|
try
|
|
echo foreplay#eval(s:todo)
|
|
catch /^Clojure:/
|
|
endtry
|
|
return ''
|
|
endfunction
|
|
|
|
function! s:editop(type) abort
|
|
call feedkeys(&cedit . "\<Home>", 'n')
|
|
let input = s:input(substitute(substitute(s:opfunc(a:type), "\s*;[^\n]*", '', 'g'), '\n\+\s*', ' ', 'g'))
|
|
try
|
|
if input !=# ''
|
|
echo foreplay#eval(input)
|
|
endif
|
|
catch /^Clojure:/
|
|
return ''
|
|
endtry
|
|
endfunction
|
|
|
|
function! s:Eval(bang, line1, line2, count, args) abort
|
|
if a:args !=# ''
|
|
let expr = a:args
|
|
else
|
|
if a:count ==# 0
|
|
normal! ^
|
|
let line1 = searchpair('(','',')', 'bcrn', g:foreplay#skip)
|
|
let line2 = searchpair('(','',')', 'rn', g:foreplay#skip)
|
|
else
|
|
let line1 = a:line1
|
|
let line2 = a:line2
|
|
endif
|
|
if !line1 || !line2
|
|
return ''
|
|
endif
|
|
let expr = join(getline(line1, line2), "\n")
|
|
if a:bang
|
|
exe line1.','.line2.'delete _'
|
|
endif
|
|
endif
|
|
try
|
|
let result = foreplay#eval(expr)
|
|
if a:bang
|
|
if a:args !=# ''
|
|
call append(a:line1, result)
|
|
exe a:line1
|
|
else
|
|
call append(a:line1-1, result)
|
|
exe a:line1-1
|
|
endif
|
|
else
|
|
echo result
|
|
endif
|
|
catch /^Clojure:/
|
|
endtry
|
|
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(...)
|
|
return call(function('input'), a:000)
|
|
endfunction
|
|
|
|
function! s:input(default) abort
|
|
if !exists('g:FOREPLAY_HISTORY')
|
|
let g:FOREPLAY_HISTORY = []
|
|
endif
|
|
try
|
|
let s:input = bufnr('%')
|
|
let s:oldhist = s:histswap(g:FOREPLAY_HISTORY)
|
|
return s:actually_input(foreplay#ns().'=> ', a:default, 'customlist,foreplay#eval_complete')
|
|
finally
|
|
unlet! s:input
|
|
if exists('s:oldhist')
|
|
let g:FOREPLAY_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 ")\<CR>"
|
|
else
|
|
return ")"
|
|
endif
|
|
endfunction
|
|
|
|
function! s:inputeval() abort
|
|
let input = s:input('')
|
|
redraw
|
|
if input ==# ''
|
|
return ''
|
|
else
|
|
try
|
|
echo foreplay#eval(input)
|
|
return ''
|
|
catch /^Clojure:/
|
|
return ''
|
|
catch
|
|
return 'echoerr '.string(v:exception)
|
|
endtry
|
|
endif
|
|
endfunction
|
|
|
|
function! s:recall() abort
|
|
try
|
|
cnoremap <expr> ) <SID>inputclose()
|
|
let input = s:input('(')
|
|
if input =~# '^(\=$'
|
|
return ''
|
|
else
|
|
return foreplay#eval(input)
|
|
endif
|
|
catch /^Clojure:/
|
|
return ''
|
|
finally
|
|
silent! cunmap )
|
|
endtry
|
|
endfunction
|
|
|
|
function! s:histswap(list)
|
|
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 <silent> <Plug>ForeplayPrintLast :exe <SID>print_last()<CR>
|
|
nnoremap <silent> <Plug>ForeplayPrint :<C-U>set opfunc=<SID>printop<CR>g@
|
|
xnoremap <silent> <Plug>ForeplayPrint :<C-U>call <SID>printop(visualmode())<CR>
|
|
|
|
nnoremap <silent> <Plug>ForeplayFilter :<C-U>set opfunc=<SID>filterop<CR>g@
|
|
xnoremap <silent> <Plug>ForeplayFilter :<C-U>call <SID>filterop(visualmode())<CR>
|
|
|
|
nnoremap <silent> <Plug>ForeplayEdit :<C-U>set opfunc=<SID>editop<CR>g@
|
|
xnoremap <silent> <Plug>ForeplayEdit :<C-U>call <SID>editop(visualmode())<CR>
|
|
|
|
nnoremap <Plug>ForeplayPrompt :exe <SID>inputeval()<CR>
|
|
|
|
noremap! <Plug>ForeplayRecall <C-R>=<SID>recall()<CR>
|
|
|
|
function! s:setup_eval() abort
|
|
command! -buffer -bang -range=0 -nargs=? -complete=customlist,foreplay#eval_complete Eval :exe s:Eval(<bang>0, <line1>, <line2>, <count>, <q-args>)
|
|
|
|
nmap <buffer> cp <Plug>ForeplayPrint
|
|
nmap <buffer> cpp <Plug>ForeplayPrintab
|
|
|
|
nmap <buffer> c! <Plug>ForeplayFilter
|
|
nmap <buffer> c!! <Plug>ForeplayFilterab
|
|
|
|
nmap <buffer> cq <Plug>ForeplayEdit
|
|
nmap <buffer> cqq <Plug>ForeplayEditab
|
|
|
|
nmap <buffer> cqp <Plug>ForeplayPrompt
|
|
exe 'nmap <buffer> cqc <Plug>ForeplayPrompt' . &cedit . 'i'
|
|
|
|
map! <buffer> <C-R>( <Plug>ForeplayRecall
|
|
endfunction
|
|
|
|
function! s:cmdwinenter()
|
|
setlocal filetype=clojure
|
|
endfunction
|
|
|
|
function! s:cmdwinleave()
|
|
setlocal filetype< omnifunc<
|
|
endfunction
|
|
|
|
augroup foreplay_eval
|
|
autocmd!
|
|
autocmd FileType clojure call s:setup_eval()
|
|
autocmd CmdWinEnter @ if exists('s:input') | call s:cmdwinenter() | endif
|
|
autocmd CmdWinLeave @ if exists('s:input') | call s:cmdwinleave() | endif
|
|
augroup END
|
|
|
|
" }}}1
|
|
" :Require {{{1
|
|
|
|
function! s:Require(bang, ns)
|
|
let cmd = ('(require '.s:qsym(a:ns ==# '' ? foreplay#ns() : a:ns).' :reload'.(a:bang ? '-all' : '').')')
|
|
echo cmd
|
|
try
|
|
call foreplay#eval(cmd)
|
|
return ''
|
|
catch /^Clojure:.*/
|
|
return ''
|
|
endtry
|
|
endfunction
|
|
|
|
function! s:setup_require()
|
|
command! -buffer -bar -bang -complete=customlist,foreplay#ns_complete -nargs=? Require :exe s:Require(<bang>0, <q-args>)
|
|
nnoremap <silent><buffer> cpr :Require<CR>
|
|
nnoremap <silent><buffer> cpR :Require!<CR>
|
|
endfunction
|
|
|
|
augroup foreplay_require
|
|
autocmd!
|
|
autocmd FileType clojure call s:setup_require()
|
|
augroup END
|
|
|
|
" }}}1
|
|
" Go to source {{{1
|
|
|
|
function! foreplay#source(symbol) abort
|
|
let c = foreplay#local_client()
|
|
call c.require(foreplay#ns())
|
|
let cmd =
|
|
\ " (when-let [v (resolve " . s:qsym(a:symbol) .')]' .
|
|
\ ' (when-let [filepath (:file (meta v))]' .
|
|
\ ' (when-let [url (.getResource (clojure.lang.RT/baseLoader) filepath)]' .
|
|
\ ' (symbol (str (str "+" (:line (meta v))) " "' .
|
|
\ ' (if (= "jar" (.getProtocol url))' .
|
|
\ ' (str "zip" (.replaceFirst (.getFile url) "!/" "::"))' .
|
|
\ ' (.getFile url)))))))'
|
|
let result = get(split(c.eval(cmd, foreplay#ns()).value, "\n"), 0, '')
|
|
return result ==# 'nil' ? '' : result
|
|
endfunction
|
|
|
|
function! s:Edit(cmd, keyword) abort
|
|
if a:keyword =~# '^\k\+/$'
|
|
let location = foreplay#findfile(a:keyword[0: -2])
|
|
elseif a:keyword =~# '^\k\+\.[^/.]\+$'
|
|
let location = foreplay#findfile(a:keyword)
|
|
else
|
|
let location = foreplay#source(a:keyword)
|
|
endif
|
|
if location !=# ''
|
|
if matchstr(location, '^+\d\+ \zs.*') ==# expand('%:p') && a:cmd ==# 'edit'
|
|
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
|
|
|
|
augroup foreplay_source
|
|
autocmd!
|
|
autocmd FileType clojure setlocal includeexpr=tr(v:fname,'.-','/_')
|
|
autocmd FileType clojure setlocal suffixesadd=.clj,.java
|
|
autocmd FileType clojure setlocal define=^\\s*(def\\w*
|
|
autocmd FileType clojure command! -bar -buffer -nargs=1 -complete=customlist,foreplay#eval_complete Djump :exe s:Edit('edit', <q-args>)
|
|
autocmd FileType clojure command! -bar -buffer -nargs=1 -complete=customlist,foreplay#eval_complete Dsplit :exe s:Edit('split', <q-args>)
|
|
autocmd FileType clojure nnoremap <silent><buffer> [<C-D> :<C-U>exe <SID>Edit('edit', expand('<cword>'))<CR>
|
|
autocmd FileType clojure nnoremap <silent><buffer> ]<C-D> :<C-U>exe <SID>Edit('edit', expand('<cword>'))<CR>
|
|
autocmd FileType clojure nnoremap <silent><buffer> <C-W><C-D> :<C-U>exe <SID>Edit('split', expand('<cword>'))<CR>
|
|
autocmd FileType clojure nnoremap <silent><buffer> <C-W>d :<C-U>exe <SID>Edit('split', expand('<cword>'))<CR>
|
|
autocmd FileType clojure nnoremap <silent><buffer> <C-W>gd :<C-U>exe <SID>Edit('tabedit', expand('<cword>'))<CR>
|
|
augroup END
|
|
|
|
" }}}1
|
|
" Go to file {{{1
|
|
|
|
function! foreplay#findfile(path) abort
|
|
let c = foreplay#local_client()
|
|
call c.require(foreplay#ns())
|
|
|
|
let cmd =
|
|
\ '(symbol' .
|
|
\ ' (or' .
|
|
\ ' (when-let [url (.getResource (clojure.lang.RT/baseLoader) %s)]' .
|
|
\ ' (if (= "jar" (.getProtocol url))' .
|
|
\ ' (str "zip" (.replaceFirst (.getFile url) "!/" "::"))' .
|
|
\ ' (.getFile url)))' .
|
|
\ ' ""))'
|
|
|
|
let path = a:path
|
|
|
|
if path !~# '[/.]' && path =~# '^\k\+$'
|
|
let aliascmd = printf(cmd,
|
|
\ '(if-let [ns ((ns-aliases *ns*) '.s:qsym(path).')]' .
|
|
\ ' (str (.replace (.replace (str (ns-name ns)) "-" "_") "." "/") ".clj")' .
|
|
\ ' "'.path.'.clj")')
|
|
let result = get(split(c.eval(aliascmd, foreplay#ns()).value, "\n"), 0, '')
|
|
else
|
|
if path !~# '/'
|
|
let path = tr(path, '.-', '/_')
|
|
endif
|
|
if path !~# '\.\w\+$'
|
|
let path .= '.clj'
|
|
endif
|
|
|
|
let result = get(split(c.eval(printf(cmd, '"'.escape(path, '"').'"'), foreplay#ns()).value, "\n"), 0, '')
|
|
|
|
endif
|
|
if result ==# ''
|
|
return findfile(path, &l:path)
|
|
else
|
|
return result
|
|
endif
|
|
endfunction
|
|
|
|
function! s:GF(cmd, file) abort
|
|
if a:file =~# '^[^/]*/[^/.]*$' && a:file =~# '^\k\+$'
|
|
let [file, jump] = split(a:file, "/")
|
|
else
|
|
let file = a:file
|
|
endif
|
|
let file = foreplay#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
|
|
|
|
augroup foreplay_go_to_file
|
|
autocmd!
|
|
autocmd FileType clojure nnoremap <silent><buffer> gf :<C-U>exe <SID>GF('edit', expand('<cfile>'))<CR>
|
|
autocmd FileType clojure nnoremap <silent><buffer> <C-W>f :<C-U>exe <SID>GF('split', expand('<cfile>'))<CR>
|
|
autocmd FileType clojure nnoremap <silent><buffer> <C-W><C-F> :<C-U>exe <SID>GF('split', expand('<cfile>'))<CR>
|
|
autocmd FileType clojure nnoremap <silent><buffer> <C-W>gf :<C-U>exe <SID>GF('tabedit', expand('<cfile>'))<CR>
|
|
augroup END
|
|
|
|
" }}}1
|
|
" Documentation {{{1
|
|
|
|
function! s:buffer_path(...) abort
|
|
let buffer = a:0 ? a:1 : exists('s:input') ? s:input : '%'
|
|
if getbufvar(buffer, '&buftype') =~# '^no'
|
|
return ''
|
|
endif
|
|
let path = substitute(fnamemodify(bufname(buffer), ':p'), '\C^zipfile:\(.*\)::', '\1/', '')
|
|
for dir in classpath#split(classpath#from_vim(getbufvar(buffer, '&path')))
|
|
if dir !=# '' && path[0 : strlen(dir)-1] ==# dir
|
|
return path[strlen(dir)+1:-1]
|
|
endif
|
|
endfor
|
|
return ''
|
|
endfunction
|
|
|
|
function! s:tons(path) abort
|
|
return tr(substitute(a:path, '\.\w\+$', '', ''), '\/_', '..-')
|
|
endfunction
|
|
|
|
function! foreplay#ns() abort
|
|
let lnum = 1
|
|
while lnum < line('$') && getline(lnum) =~# '^\s*\%(;.*\)\=$'
|
|
let lnum += 1
|
|
endwhile
|
|
let ns = matchstr(getline(lnum), '\C^(\s*\%(in-ns\s*''\|ns\s\+\)\zs\k\+\ze')
|
|
if ns !=# ''
|
|
return ns
|
|
endif
|
|
let path = s:buffer_path()
|
|
return s:tons(path ==# '' ? 'user' : path)
|
|
endfunction
|
|
|
|
function! s:Lookup(ns, macro, arg) abort
|
|
" doc is in clojure.core in older Clojure versions
|
|
try
|
|
call foreplay#eval("(require '".a:ns.") (eval (list (if (ns-resolve 'clojure.core '".a:macro.") 'clojure.core/".a:macro." '".a:ns.'/'.a:macro.") '".a:arg.'))')
|
|
catch /^Clojure:/
|
|
catch /.*/
|
|
echohl ErrorMSG
|
|
echo v:exception
|
|
echohl None
|
|
endtry
|
|
return ''
|
|
endfunction
|
|
|
|
function! s:inputlist(label, entries)
|
|
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:Apropos(pattern) abort
|
|
if a:pattern =~# '^#\="'
|
|
let pattern = a:pattern
|
|
elseif a:pattern =~# '^^'
|
|
let pattern = '#"' . a:pattern . '"'
|
|
else
|
|
let pattern = '"' . a:pattern . '"'
|
|
endif
|
|
let matches = foreplay#evalparse('(apropos '.pattern.')')
|
|
if empty(matches)
|
|
return ''
|
|
endif
|
|
let choice = s:inputlist('Look up docs for:', matches)
|
|
if choice !=# ''
|
|
return 'echo "\n"|Doc '.choice
|
|
else
|
|
return ''
|
|
endif
|
|
endfunction
|
|
|
|
function! s:K()
|
|
let word = expand('<cword>')
|
|
let java_candidate = matchstr(word, '^\%(\w\+\.\)*\u\w*\ze\%(\.\|\/\w\+\)\=$')
|
|
if java_candidate !=# ''
|
|
return 'Javadoc '.java_candidate
|
|
else
|
|
return 'Doc '.word
|
|
endif
|
|
endfunction
|
|
|
|
augroup foreplay_doc
|
|
autocmd!
|
|
autocmd FileType clojure nnoremap <buffer> K :<C-R>=<SID>K()<CR><CR>
|
|
autocmd FileType clojure nnoremap <buffer> [d :Source <C-R><C-W><CR>
|
|
autocmd FileType clojure nnoremap <buffer> ]d :Source <C-R><C-W><CR>
|
|
autocmd FileType clojure command! -buffer -nargs=1 Apropos :exe s:Apropos(<q-args>)
|
|
autocmd FileType clojure command! -buffer -nargs=1 FindDoc :exe s:Lookup('clojure.repl', 'find-doc', printf('#"%s"', <q-args>))
|
|
autocmd FileType clojure command! -buffer -bar -nargs=1 Javadoc :exe s:Lookup('clojure.java.javadoc', 'javadoc', <q-args>)
|
|
autocmd FileType clojure command! -buffer -bar -nargs=1 -complete=customlist,foreplay#eval_complete Doc :exe s:Lookup('clojure.repl', 'doc', <q-args>)
|
|
autocmd FileType clojure command! -buffer -bar -nargs=1 -complete=customlist,foreplay#eval_complete Source :exe s:Lookup('clojure.repl', 'source', <q-args>)
|
|
augroup END
|
|
|
|
" }}}1
|
|
" Leiningen {{{1
|
|
|
|
function! s:hunt(start, anchor) abort
|
|
let root = simplify(fnamemodify(a:start, ':p:s?[\/]$??'))
|
|
if !isdirectory(fnamemodify(root, ':h'))
|
|
return ''
|
|
endif
|
|
let previous = ""
|
|
while root !=# previous
|
|
if filereadable(root . '/' . a:anchor) && isdirectory(root . '/src')
|
|
return root
|
|
endif
|
|
let previous = root
|
|
let root = fnamemodify(root, ':h')
|
|
endwhile
|
|
return ''
|
|
endfunction
|
|
|
|
if !exists('s:leiningen_repl_ports')
|
|
let s:leiningen_repl_ports = {}
|
|
endif
|
|
|
|
function! s:leiningen_connect()
|
|
if !exists('b:leiningen_root')
|
|
return
|
|
endif
|
|
let portfile = b:leiningen_root . '/target/repl-port'
|
|
if getfsize(portfile) > 0 && getftime(portfile) !=# get(s:leiningen_repl_ports, b:leiningen_root, -1)
|
|
let port = readfile(portfile, 'b', 1)[0]
|
|
let s:leiningen_repl_ports[b:leiningen_root] = getftime(portfile)
|
|
try
|
|
call s:register_connection(nrepl#foreplay_connection#open(port), b:leiningen_root)
|
|
catch /^nREPL: Connection/
|
|
call delete(portfile)
|
|
endtry
|
|
endif
|
|
endfunction
|
|
|
|
function! s:leiningen_init() abort
|
|
|
|
if !exists('b:leiningen_root')
|
|
let root = s:hunt(expand('%:p'), 'project.clj')
|
|
if root !=# ''
|
|
let b:leiningen_root = root
|
|
endif
|
|
endif
|
|
if !exists('b:leiningen_root')
|
|
return
|
|
endif
|
|
|
|
let b:java_root = b:leiningen_root
|
|
|
|
setlocal makeprg=lein efm=%+G
|
|
|
|
call s:leiningen_connect()
|
|
endfunction
|
|
|
|
augroup foreplay_leiningen
|
|
autocmd!
|
|
autocmd User ForeplayPreConnect call s:leiningen_connect()
|
|
autocmd FileType clojure call s:leiningen_init()
|
|
augroup END
|
|
|
|
" }}}1
|
|
|
|
" vim:set et sw=2:
|