" fireplace.vim - Clojure REPL tease " Maintainer: Tim Pope if exists("g:loaded_fireplace") || v:version < 700 || &cp finish endif let g:loaded_fireplace = 1 " File type {{{1 augroup fireplace_file_type autocmd! autocmd BufNewFile,BufReadPost *.clj setfiletype clojure augroup END " }}}1 " Escaping {{{1 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 " }}}1 " Completion {{{1 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 " }}}1 " REPL client {{{1 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') try let result = clone.eval('(ns '.self.user_ns().' (:require '.a:lib.reload.'))', {'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 if empty(a:arg) let arg = '' else let arg = ' :repl-env ' . a:arg endif let connection = s:conn_try(self.connection, 'clone') 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 = nrepl#fireplace_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 " }}}1 " :Connect {{{1 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, ":h:t")') 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 = {proto}#fireplace_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 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}#fireplace_connection#prompt() endif try let connection = {proto}#fireplace_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! -bar -complete=customlist,s:connect_complete -nargs=* Connect :FireplaceConnect autocmd FileType clojure command! -complete=customlist,fireplace#eval_complete -bang -nargs=* Piggieback :call s:piggieback(, 0) augroup END " }}}1 " Java runner {{{1 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) 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 return s:spawning_eval(join(self.path(), has('win32') ? ';' : ':'), \ a:expr, get(a:options, 'ns', self.user_ns())) endfunction function! s:oneoff.message(...) abort throw 'Fireplace: no live REPL connection' endfunction let s:oneoff.piggieback = s:oneoff.message " }}}1 " Client {{{1 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 " }}}1 " Quickfix {{{1 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 " }}}1 " Eval {{{1 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(&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:setup_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:setup_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:setup_eval() autocmd BufReadPost * if has_key(s:qffiles, expand(':p')) | \ call s:setup_historical() | \ endif 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, 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:setup_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:setup_require() augroup END " }}}1 " Go to source {{{1 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('')) augroup fireplace_source autocmd! autocmd FileType clojure setlocal includeexpr=tr(v:fname,'.-','/_') autocmd FileType clojure \ if expand('%:e') ==# 'cljs' | \ setlocal suffixesadd=.cljs,.cljx,.clj,.java | \ else | \ setlocal suffixesadd=.clj,.cljx,.cljs,.java | \ endif autocmd FileType clojure setlocal define=^\\s*(def\\w* autocmd FileType clojure command! -bar -buffer -nargs=1 -complete=customlist,fireplace#eval_complete Djump :exe s:Edit('edit', ) autocmd FileType clojure command! -bar -buffer -nargs=1 -complete=customlist,fireplace#eval_complete Dsplit :exe s:Edit('split', ) autocmd FileType clojure nmap [ FireplaceDjump autocmd FileType clojure nmap ] FireplaceDjump autocmd FileType clojure nmap FireplaceDsplit autocmd FileType clojure nmap d FireplaceDsplit autocmd FileType clojure nmap gd FireplaceDtabjump augroup END " }}}1 " Go to file {{{1 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 augroup fireplace_go_to_file autocmd! autocmd FileType clojure nnoremap gf :exe GF('edit', expand('')) autocmd FileType clojure nnoremap f :exe GF('split', expand('')) autocmd FileType clojure nnoremap :exe GF('split', expand('')) autocmd FileType clojure nnoremap gf :exe GF('tabedit', expand('')) augroup END " }}}1 " Documentation {{{1 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: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 = fireplace#evalparse('(clojure.repl/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: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 augroup fireplace_doc autocmd! autocmd FileType clojure nmap K FireplaceK autocmd FileType clojure nmap [d FireplaceSource autocmd FileType clojure nmap ]d FireplaceSource autocmd FileType clojure command! -buffer -nargs=1 Apropos :exe s:Apropos() autocmd FileType clojure command! -buffer -nargs=1 FindDoc :exe s:Lookup('clojure.repl', 'find-doc', printf('#"%s"', )) autocmd FileType clojure command! -buffer -bar -nargs=1 Javadoc :exe s:Lookup('clojure.java.javadoc', 'javadoc', ) autocmd FileType clojure command! -buffer -bar -nargs=1 -complete=customlist,fireplace#eval_complete Doc :exe s:Doc() autocmd FileType clojure command! -buffer -bar -nargs=1 -complete=customlist,fireplace#eval_complete Source :exe s:Lookup('clojure.repl', 'source', ) augroup END " }}}1 " Tests {{{1 function! fireplace#capture_test_run(expr) abort let expr = '(require ''clojure.test) ' \ . '(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:expr . ')' 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") let entry = {'text': line} if line =~# '\t.*\t.*\t' 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 endif call add(qflist, entry) endfor call setqflist(qflist) endfunction function! s:RunTests(bang, ...) abort if &autowrite || &autowriteall silent! wall endif let pre = '' if a:bang && a:0 let expr = '(clojure.test/run-all-tests #"'.join(a:000, '|').'")' elseif a:bang let expr = '(clojure.test/run-all-tests)' else 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, ' ').')' endif call fireplace#capture_test_run(pre.expr) 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', expr) return endif endfor echo expr endfunction augroup fireplace_command autocmd! autocmd FileType clojure command! -buffer -bar -bang -nargs=* \ -complete=customlist,fireplace#ns_complete RunTests \ call s:RunTests(0, ) augroup END " }}}1 " Legacy {{{1 if !empty(findfile('plugin/leiningen.vim', escape(&rtp, ' '))) finish endif augroup fireplace_alternate autocmd! autocmd FileType clojure call s:define_alternates() augroup END function! s:define_alternates() abort if exists(':A') == 2 return endif command! -buffer -bar -bang A echoerr 'Install leiningen.vim to continue using :A' command! -buffer -bar -bang AS echoerr 'Install leiningen.vim to continue using :A' command! -buffer -bar -bang AV echoerr 'Install leiningen.vim to continue using :A' command! -buffer -bar -bang AT echoerr 'Install leiningen.vim to continue using :A' endfunction " }}}1 " vim:set et sw=2: