├── 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 |
--------------------------------------------------------------------------------