├── README.markdown ├── compiler ├── boot.vim ├── clojure.vim ├── lein.vim └── shadowcljs.vim ├── doc └── salve.txt └── plugin └── salve.vim /README.markdown: -------------------------------------------------------------------------------- 1 | # salve.vim 2 | 3 | Static Vim support for [Leiningen][], [Boot][], and the [Clojure CLI][]. 4 | 5 | > Leiningen ran... [the ants] would get to him soon, despite the salve on 6 | > his boots. 7 | 8 | -- from "Leiningen versus the Ants" 9 | 10 | ## Features 11 | 12 | * `:Console` command to start a REPL or focus an existing instance if already 13 | running using [dispatch.vim][]. 14 | * Autoconnect [fireplace.vim][] to the REPL, or autostart it with `:Console`. 15 | * [Navigation commands][projectionist.vim]: `:Esource`, `:Emain`, `:Etest`, 16 | and `:Eresource`. 17 | * Alternate between test and implementation with `:A`. 18 | * Use `:make` to invoke `lein`, `boot`, or `clojure`, complete with stacktrace 19 | parsing. 20 | * Default [dispatch.vim][]'s `:Dispatch` to running the associated test file. 21 | * `'path'` is seeded with the classpath to enable certain static Vim and 22 | [fireplace.vim][] behaviors. 23 | 24 | [Leiningen]: http://leiningen.org/ 25 | [Boot]: http://boot-clj.com/ 26 | [Clojure CLI]: https://clojure.org/guides/deps_and_cli 27 | [fireplace.vim]: https://github.com/tpope/vim-fireplace 28 | [dispatch.vim]: https://github.com/tpope/vim-dispatch 29 | [projectionist.vim]: https://github.com/tpope/vim-projectionist 30 | 31 | ## Installation 32 | 33 | Install using your favorite package manager, or use Vim's built-in package 34 | support: 35 | 36 | mkdir -p ~/.vim/pack/tpope/start 37 | cd ~/.vim/pack/tpope/start 38 | git clone https://tpope.io/vim/salve.git 39 | git clone https://tpope.io/vim/projectionist.git 40 | git clone https://tpope.io/vim/dispatch.git 41 | git clone https://tpope.io/vim/fireplace.git 42 | vim -u NONE -c "helptags salve/doc" -c q 43 | vim -u NONE -c "helptags projectionist/doc" -c q 44 | vim -u NONE -c "helptags dispatch/doc" -c q 45 | vim -u NONE -c "helptags fireplace/doc" -c q 46 | 47 | ## FAQ 48 | 49 | > Why does it sometimes take a few extra seconds for Vim to startup? 50 | 51 | Much of the functionality of salve.vim depends on knowing the classpath. 52 | When possible, this is retrieved from a [fireplace.vim][] connection, but if 53 | not, this means a call to `lein classpath` or `boot show --fake-classpath`. 54 | 55 | Once retrieved, the classpath is cached until a project manifest file 56 | changes: for Leiningen `project.clj` or `~/.lein/profiles.clj`, for Boot 57 | `build.boot` or `~/.boot/profile.boot`, for the Clojure CLI `deps.edn` or 58 | `~/.clojure/deps.edn`. 59 | 60 | ## License 61 | 62 | Copyright © Tim Pope. Distributed under the same terms as Vim itself. 63 | See `:help license`. 64 | -------------------------------------------------------------------------------- /compiler/boot.vim: -------------------------------------------------------------------------------- 1 | " Vim compiler file 2 | 3 | if exists("current_compiler") 4 | finish 5 | endif 6 | let current_compiler = "boot" 7 | 8 | CompilerSet makeprg=boot 9 | CompilerSet errorformat=%+G%.%# 10 | \,%\\&default=test 11 | \,%\\&start=repl 12 | \,%\\&force_start=repl%\\>%\\ze%.%# 13 | -------------------------------------------------------------------------------- /compiler/clojure.vim: -------------------------------------------------------------------------------- 1 | " Vim compiler file 2 | 3 | if exists("current_compiler") 4 | finish 5 | endif 6 | let current_compiler = "clojure" 7 | 8 | CompilerSet makeprg=clojure 9 | " CompilerSet makeprg=clj 10 | CompilerSet errorformat=%+G%.%# 11 | \,%\\&default=-A:default 12 | \,%\\&start=-Sdeps\ %%:p:h:s?.*?\\=g:salve_edn_deps?:S\ -m\ nrepl.cmdline\ --interactive\ --middleware\ %%:p:h:s?.*?\\=g:salve_edn_middleware?:S 13 | -------------------------------------------------------------------------------- /compiler/lein.vim: -------------------------------------------------------------------------------- 1 | " Vim compiler file 2 | 3 | if exists("current_compiler") 4 | finish 5 | endif 6 | let current_compiler = "lein" 7 | 8 | CompilerSet makeprg=lein 9 | CompilerSet errorformat=%+G%.%# 10 | \,%\\&default=test 11 | \,%\\&start=repl 12 | \,%\\&force_start=repl%\\>%\\ze%.%# 13 | -------------------------------------------------------------------------------- /compiler/shadowcljs.vim: -------------------------------------------------------------------------------- 1 | " Vim compiler file 2 | 3 | if exists("current_compiler") 4 | finish 5 | endif 6 | let current_compiler = "shadowcljs" 7 | 8 | CompilerSet makeprg=npx\ shadow-cljs 9 | " CompilerSet makeprg=shadow-cljs 10 | CompilerSet errorformat=%+G%.%# 11 | \,%\\&default=test 12 | \,%\\&start=clj-repl 13 | \,%\\&force_start=%\\w%\\+-repl%\\>%\\ze%.%# 14 | -------------------------------------------------------------------------------- /doc/salve.txt: -------------------------------------------------------------------------------- 1 | *salve.txt* Static support for Leiningen/Boot 2 | 3 | Author: Tim Pope 4 | License: Same terms as Vim itself (see |license|) 5 | 6 | INTRODUCTION *salve* 7 | 8 | Salve.vim automatically activates when it finds a project.clj, build.boot, or 9 | deps.edn in a parent directory of the current file. Most functionality 10 | depends on the classpath, so `lein classpath`, `boot show --fake-classpath`, 11 | or `clojure -Spath` is run on an initial load and cached until the project 12 | manifest or global config file changes. 13 | 14 | If `g:salve_auto_start_repl` is set, salve.vim will attempt to make a REPL 15 | connection for you. 16 | 17 | REPL SUPPORT *salve-repl* 18 | 19 | Each time |fireplace| attempts to use a repl connection, salve.vim will 20 | try to automatically connect using target/repl-port or target/repl/repl-port. 21 | (Fireplace itself supports the newer standard .nrepl-port.) If none of the 3 22 | port files exist, :Console! (see below) is invoked to automatically start a 23 | repl. 24 | 25 | *salve-:Console* 26 | :Console Invoke `lein repl`, `boot repl`, or 27 | `clojure -m nrepl.cmdline` using |:dispatch-:Start|. 28 | 29 | :Console! Like :Console, but start in the background. 30 | 31 | If a suitably fresh repl is running when an updated classpath is required, it 32 | will be used instead of shelling out to determine the classpath. 33 | 34 | COMPILER *salve-compiler* 35 | 36 | The appropriate included compiler plugin is automatically invoked so that 37 | |:make| (or |dispatch-:Make|) will invoke `lein`, `boot`, or `clojure`. The 38 | 'errorformat' is adjusted to include the classpath, so that plugins like 39 | |fireplace| can parse stack traces. 40 | 41 | PROJECTIONS *salve-projections* 42 | 43 | Leiningen/Boot projects are |projectionist| projects: 44 | 45 | * Alternating jumps between test and implementation. 46 | * The default :Start task is the same command run by |salve-:Console|. 47 | * The default :Dispatch task is to run the tests for the current namespace. 48 | 49 | Additionally, the following navigation commands are provided: 50 | 51 | *salve-:Emain* 52 | :Emain {file} Edit a Clojure project file relative to any source 53 | (not test or resource) directory in the classpath. 54 | 55 | *salve-:Esource* 56 | :Esource {file} Edit a Clojure project file relative to the classpath. 57 | 58 | *salve-:Etest* 59 | :Etest {file} Edit a file matching one of **/*-test.clj, 60 | **/test/*.clj, **/t_*.clj, or **/*_spec.clj, relative 61 | to any project test directory in the classpath. 62 | 63 | *salve-:Eresource* 64 | :Eresource {file} Edit an arbitrary project file relative to the 65 | classpath. 66 | 67 | ABOUT *salve-about* 68 | 69 | Grab the latest version or report a bug on GitHub: 70 | 71 | http://github.com/tpope/vim-salve 72 | 73 | vim:tw=78:et:ft=help:norl: 74 | -------------------------------------------------------------------------------- /plugin/salve.vim: -------------------------------------------------------------------------------- 1 | " Location: plugin/salve.vim 2 | " Author: Tim Pope 3 | 4 | if exists('g:loaded_salve') || v:version < 800 5 | finish 6 | endif 7 | let g:loaded_salve = 1 8 | 9 | if !exists('g:classpath_cache') 10 | let g:classpath_cache = '~/.cache/vim/classpath' 11 | endif 12 | 13 | if !exists('g:salve_edn_deps') 14 | let g:salve_edn_deps = '{:deps {cider/cider-nrepl {:mvn/version "RELEASE"} }}' 15 | endif 16 | if !exists('g:salve_edn_middleware') 17 | let g:salve_edn_middleware = '[cider.nrepl/cider-middleware]' 18 | endif 19 | 20 | if !isdirectory(expand(g:classpath_cache)) 21 | call mkdir(expand(g:classpath_cache), 'p') 22 | endif 23 | 24 | function! s:portfile() abort 25 | if !exists('b:salve') 26 | return '' 27 | endif 28 | 29 | let root = b:salve.root 30 | let portfiles = get(b:salve, 'portfiles', []) + [root.'/.nrepl-port', root.'/target/repl-port', root.'/target/repl/repl-port'] 31 | 32 | for f in portfiles 33 | if getfsize(f) > 0 34 | return f 35 | endif 36 | endfor 37 | return '' 38 | endfunction 39 | 40 | function! s:repl(background, args) abort 41 | let args = empty(a:args) ? '' : ' ' . a:args 42 | let portfile = s:portfile() 43 | if a:background && !empty(portfile) 44 | return 45 | endif 46 | let cd = haslocaldir() ? 'lcd' : 'cd' 47 | let cwd = getcwd() 48 | try 49 | let cmd = b:salve.start_cmd 50 | execute cd fnameescape(b:salve.root) 51 | if exists(':Start') == 2 52 | execute 'Start'.(a:background ? '!' : '') '-title=' 53 | \ . escape(fnamemodify(b:salve.root, ':t') . ' repl', ' ') 54 | \ cmd.args 55 | if get(get(g:, 'dispatch_last_start', {}), 'handler', 'headless') ==# 'headless' 56 | return 57 | endif 58 | elseif a:background 59 | echohl WarningMsg 60 | echomsg "Can't start background console without dispatch.vim" 61 | echohl None 62 | return 63 | elseif has('win32') 64 | execute '!start '.cmd.args 65 | else 66 | execute '!'.cmd.args 67 | return 68 | endif 69 | finally 70 | execute cd fnameescape(cwd) 71 | endtry 72 | 73 | let i = 0 74 | while empty(portfile) && i < 300 && !getchar(0) 75 | let i += 1 76 | sleep 100m 77 | let portfile = s:portfile() 78 | endwhile 79 | endfunction 80 | 81 | function! s:connect(autostart) abort 82 | if !exists('b:salve') || !exists(':FireplaceConnect') 83 | return {} 84 | endif 85 | let portfile = s:portfile() 86 | if exists('g:salve_auto_start_repl') && a:autostart && empty(portfile) && exists(':Start') ==# 2 87 | call s:repl(1, '') 88 | let portfile = s:portfile() 89 | endif 90 | 91 | try 92 | return empty(portfile) ? {} : 93 | \ fireplace#register_port_file(portfile, b:salve.root) 94 | catch 95 | return {} 96 | endtry 97 | endfunction 98 | 99 | function! s:fcall(fn, path, ...) abort 100 | let ns = matchstr(a:path, '^\a\a\+\ze:') 101 | if len(ns) && exists('*' . ns . '#' . a:fn) 102 | return call(ns . '#' . a:fn, [a:path] + a:000) 103 | else 104 | return call(a:fn, [a:path] + a:000) 105 | endif 106 | endfunction 107 | 108 | function! s:filereadable(path) abort 109 | if exists('*ProjectionistHas') 110 | return call('ProjectionistHas', [a:path]) 111 | else 112 | return s:fcall('filereadable', a:path) 113 | endif 114 | endfunction 115 | 116 | function! s:detect(file) abort 117 | if !exists('b:salve') 118 | let root = a:file 119 | let previous = "" 120 | while root !=# previous && root !~# '^\.\=$\|^[\/][\/][^\/]*$' 121 | if s:filereadable(root . '/project.clj') && join(s:fcall('readfile', root . '/project.clj', '', 50)) =~# '(\s*defproject\%(\s*{{\)\@!' 122 | let b:salve = { 123 | \ "local_manifest": root.'/project.clj', 124 | \ "global_manifest": expand('~/.lein/profiles.clj'), 125 | \ "root": root, 126 | \ "compiler": "lein", 127 | \ "classpath_cmd": "lein -o classpath", 128 | \ "start_cmd": "lein repl"} 129 | let b:java_root = root 130 | break 131 | elseif s:filereadable(root . '/build.boot') 132 | let boot_home = len($BOOT_HOME) ? $BOOT_HOME : expand('~/.boot') 133 | let b:salve = { 134 | \ "local_manifest": root.'/build.boot', 135 | \ "global_manifest": boot_home.'/profile.boot', 136 | \ "root": root, 137 | \ "compiler": "boot", 138 | \ "classpath_cmd": "boot show --fake-classpath", 139 | \ "start_cmd": "boot repl"} 140 | let b:java_root = root 141 | break 142 | elseif s:filereadable(root . '/deps.edn') 143 | let b:salve = { 144 | \ "local_manifest": root.'/deps.edn', 145 | \ "global_manifest": expand('~/.clojure/deps.edn'), 146 | \ "root": root, 147 | \ "compiler": "clojure", 148 | \ "classpath_cmd": "clojure -Spath", 149 | \ "start_cmd": "clojure -Sdeps " . shellescape(g:salve_edn_deps) . " -m nrepl.cmdline --interactive --middleware " . shellescape(g:salve_edn_middleware)} 150 | let b:java_root = root 151 | elseif s:filereadable(root . '/shadow-cljs.edn') 152 | let b:salve = { 153 | \ "local_manifest": root . '/shadow-cljs.edn', 154 | \ "global_manifest": expand('~/.shadow-cljs/config.edn'), 155 | \ "root": root, 156 | \ "compiler": "shadowcljs", 157 | \ "portfiles": [root . "/.shadow-cljs/nrepl.port"], 158 | \ "classpath_cmd": "npx shadow-cljs classpath", 159 | \ "start_cmd": "npx shadow-cljs clj-repl"} 160 | endif 161 | let previous = root 162 | let root = fnamemodify(root, ':h') 163 | endwhile 164 | endif 165 | return exists('b:salve') 166 | endfunction 167 | 168 | function! s:split(path) abort 169 | return split(a:path, has('win32') ? ';' : ':') 170 | endfunction 171 | 172 | function! s:absolute(path, parent) abort 173 | if a:path =~# '^/\|^\a\+:' 174 | return a:path 175 | else 176 | return a:parent . (exists('+shellslash') && !&shellslash ? '\' : '/') . a:path 177 | endif 178 | endfunction 179 | 180 | function! s:scrape_path() abort 181 | let cd = haslocaldir() ? 'lcd' : 'cd' 182 | let cwd = getcwd() 183 | try 184 | execute cd fnameescape(b:salve.root) 185 | let path = matchstr(system(b:salve.classpath_cmd), "[^\n]*\\ze\n*$") 186 | if v:shell_error 187 | return [] 188 | endif 189 | return map(s:split(path), 's:absolute(v:val, b:salve.root)') 190 | catch /^Vim\%((\a\+)\)\=:E472:/ 191 | return [] 192 | finally 193 | execute cd fnameescape(cwd) 194 | endtry 195 | endfunction 196 | 197 | function! s:eval(conn, code, default) abort 198 | try 199 | if has_key(a:conn, 'message') || has_key(a:conn, 'Message') 200 | let request = {'op': 'eval', 'code': a:code, 'session': '', 'ns': 'user'} 201 | for msg in has_key(a:conn, 'message') ? a:conn.message(request, type([])) : a:conn.Message(request, type([])) 202 | if has_key(msg, 'value') 203 | return msg.value 204 | endif 205 | endfor 206 | endif 207 | catch 208 | endtry 209 | return a:default 210 | endfunction 211 | 212 | function! s:my_paths(path) abort 213 | return map(filter(copy(a:path), 214 | \ 'strpart(v:val, 0, len(b:salve.root)) ==# b:salve.root'), 215 | \ 'v:val[strlen(b:salve.root)+1:-1]') 216 | endfunction 217 | 218 | function! s:path() abort 219 | let projts = getftime(b:salve.local_manifest) 220 | let profts = getftime(b:salve.global_manifest) 221 | let cache = expand(g:classpath_cache . '/') . substitute(b:salve.root, '[:\/]', '%', 'g') 222 | 223 | let ts = getftime(cache) 224 | if ts > projts && ts > profts 225 | let path = split(get(readfile(cache), 0, ''), ',') 226 | 227 | elseif b:salve.compiler !=# 'clojure' 228 | let conn = s:connect(0) 229 | let ts = +s:eval(conn, '(.getStartTime (java.lang.management.ManagementFactory/getRuntimeMXBean))', '-2000')[0:-4] 230 | if ts > projts && ts > profts 231 | let value = s:eval(conn, '[(System/getProperty "path.separator") (or (System/getProperty "fake.class.path") (System/getProperty "java.class.path") "")]', '') 232 | if len(value) > 8 233 | let path = split(eval(value[5:-2]), value[2]) 234 | if empty(s:my_paths(path)) 235 | unlet path 236 | else 237 | call writefile([join(path, ',')], cache) 238 | endif 239 | endif 240 | endif 241 | endif 242 | 243 | if !exists('path') 244 | let path = s:scrape_path() 245 | if empty(path) 246 | let path = map(['test', 'src', 'dev-resources', 'resources'], 'b:salve.root."/".v:val') 247 | endif 248 | call writefile([join(path, ',')], cache) 249 | endif 250 | 251 | return path 252 | endfunction 253 | 254 | function! s:activate() abort 255 | if !exists('b:salve') 256 | return 257 | endif 258 | command! -buffer -bar -bang -nargs=* Console call s:repl(0, ) 259 | execute 'compiler' b:salve.compiler 260 | let &l:errorformat .= ',%\&' . escape('dir='.b:salve.root, '\,') 261 | let &l:errorformat .= ',%\&' . escape('classpath='.join(s:path(), ','), '\,') 262 | if get(b:, 'dispatch') =~# ':RunTests ' 263 | let &l:errorformat .= ',%\&buffer=test ' . matchstr(b:dispatch, ':RunTests \zs.*') 264 | endif 265 | if &filetype =~# '\' 266 | let &l:path = join(s:path(), ',') 267 | endif 268 | endfunction 269 | 270 | function! s:projectionist_detect() abort 271 | if !s:detect(get(g:, 'projectionist_file', get(b:, 'projectionist_file', ''))) 272 | return 273 | endif 274 | let mypaths = map(filter(copy(s:path()), 275 | \ 'strpart(v:val, 0, len(b:salve.root)) ==# b:salve.root'), 276 | \ 'v:val[strlen(b:salve.root)+1:-1]') 277 | let projections = {} 278 | let main = [] 279 | let test = [] 280 | let spec = [] 281 | for path in s:my_paths(s:path()) 282 | let projections[path.'/*'] = {'type': 'resource'} 283 | if path !~# 'target\|resources' 284 | let projections[path.'/*.clj'] = {'type': 'source', 'template': ['(ns {dot|hyphenate})']} 285 | let projections[path.'/*.cljc'] = {'type': 'source', 'template': ['(ns {dot|hyphenate})']} 286 | let projections[path.'/*.java'] = {'type': 'source'} 287 | endif 288 | if path =~# 'resource' 289 | elseif path =~# 'test' 290 | let test += [path] 291 | elseif path =~# 'spec' 292 | let spec += [path] 293 | elseif path =~# 'src' 294 | let main += [path] 295 | endif 296 | endfor 297 | call projectionist#append(b:salve.root, projections) 298 | let projections = {} 299 | 300 | let proj = {'type': 'test', 'alternate': map(copy(main), 'v:val."/{}.clj"') + 301 | \ map(copy(main), 'v:val."/{}.cljc"')} 302 | for path in test 303 | let projections[path.'/*_test.clj'] = proj 304 | let projections[path.'/*_test.cljc'] = proj 305 | let projections[path.'/**/test/*.clj'] = proj 306 | let projections[path.'/**/test/*.cljc'] = proj 307 | let projections[path.'/**/t_*.clj'] = proj 308 | let projections[path.'/**/t_*.cljc'] = proj 309 | let projections[path.'/**/test_*.clj'] = proj 310 | let projections[path.'/**/test_*.cljc'] = proj 311 | let projections[path.'/*.clj'] = {'dispatch': ':RunTests {dot|hyphenate}'} 312 | let projections[path.'/*.cljc'] = {'dispatch': ':RunTests {dot|hyphenate}'} 313 | endfor 314 | for path in spec 315 | let projections[path.'/*_spec.clj'] = proj 316 | let projections[path.'/*_spec.cljc'] = proj 317 | endfor 318 | 319 | for path in main 320 | let proj = {'type': 'main', 'alternate': map(copy(spec), 'v:val."/{}_spec.clj"')} 321 | let proj = {'type': 'main', 'alternate': map(copy(spec), 'v:val."/{}_spec.cljc"')} 322 | for tpath in test 323 | call extend(proj.alternate, [ 324 | \ tpath.'/{}_test.clj', 325 | \ tpath.'/{}_test.cljc', 326 | \ tpath.'/{dirname}/test/{basename}.clj', 327 | \ tpath.'/{dirname}/test/{basename}.cljc', 328 | \ tpath.'/{dirname}/t_{basename}.clj', 329 | \ tpath.'/{dirname}/t_{basename}.cljc', 330 | \ tpath.'/{dirname}/t_{basename}.clj', 331 | \ tpath.'/{dirname}/t_{basename}.cljc']) 332 | endfor 333 | let projections[path.'/*.clj'] = proj 334 | let projections[path.'/*.cljc'] = proj 335 | endfor 336 | call projectionist#append(b:salve.root, projections) 337 | endfunction 338 | 339 | augroup salve 340 | autocmd! 341 | autocmd User FireplacePreConnect call s:connect(1) 342 | autocmd FileType clojure 343 | \ if exists('b:salve') | 344 | \ let &l:path = join(s:path(), ',') | 345 | \ endif 346 | autocmd User ProjectionistDetect call s:projectionist_detect() 347 | autocmd User ProjectionistActivate call s:activate() 348 | autocmd BufReadPost * 349 | \ if !exists('*ProjectionistHas') && s:detect(expand('%:p')) | 350 | \ call s:activate() | 351 | \ endif 352 | augroup END 353 | --------------------------------------------------------------------------------