├── .github
└── FUNDING.yml
├── .gitignore
├── CONTRIBUTING.markdown
├── README.markdown
├── autoload
└── projectionist.vim
├── doc
└── projectionist.txt
└── plugin
└── projectionist.vim
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: tpope
2 | custom: ["https://www.paypal.me/vimpope"]
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /doc/tags
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.markdown:
--------------------------------------------------------------------------------
1 | See the [contribution guidelines for pathogen.vim](https://github.com/tpope/vim-pathogen/blob/master/CONTRIBUTING.markdown).
2 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | # projectionist.vim
2 |
3 | Projectionist provides granular project configuration using "projections".
4 | What are projections? Let's start with an example.
5 |
6 | ## Example
7 |
8 | A while back I went and made a bunch of plugins for working with [rbenv][].
9 | Here's what a couple of them look like:
10 |
11 | ~/.rbenv/plugins $ tree
12 | .
13 | ├── rbenv-ctags
14 | │ ├── bin
15 | │ │ └── rbenv-ctags
16 | │ └── etc
17 | │ └── rbenv.d
18 | │ └── install
19 | │ └── ctags.bash
20 | └── rbenv-sentience
21 | └── etc
22 | └── rbenv.d
23 | └── install
24 | └── sentience.bash
25 |
26 | As you can see, rbenv plugins have hooks in `etc/rbenv.d/` and commands in
27 | `bin/` matching `rbenv-*`. Here's a projectionist configuration for that
28 | setup:
29 |
30 | let g:projectionist_heuristics = {
31 | \ "etc/rbenv.d/|bin/rbenv-*": {
32 | \ "bin/rbenv-*": {
33 | \ "type": "command",
34 | \ "template": ["#!/usr/bin/env bash"],
35 | \ },
36 | \ "etc/rbenv.d/*.bash": {"type": "hook"}
37 | \ }
38 | \ }
39 |
40 | The key in the outermost dictionary says to activate for any directory
41 | containing a subdirectory `etc/rbenv.d/` *or* files matching `bin/rbenv-*`.
42 | The corresponding value contains projection definitions. Here, two
43 | projections are defined. The first creates an `:Ecommand` navigation command
44 | and provides boilerplate to pre-populate new files with, and the second
45 | creates an `:Ehook` command.
46 |
47 | [rails.vim]: https://github.com/tpope/vim-rails
48 | [rbenv]: https://github.com/sstephenson/rbenv
49 |
50 | ## Features
51 |
52 | See `:help projectionist` for the authoritative documentation. Here are some
53 | highlights.
54 |
55 | ### Global and per project projection definitions
56 |
57 | In the above example, we used the global `g:projectionist_heuristics` to
58 | declare projections based on requirements in the root directory. If that's
59 | not flexible enough, you can use the autocommand based API, or create a
60 | `.projections.json` in the root of the project.
61 |
62 | ### Navigation commands
63 |
64 | Navigation commands encapsulate editing filenames matching certain patterns.
65 | Here are some examples for this very project:
66 |
67 | {
68 | "plugin/*.vim": {"type": "plugin"},
69 | "autoload/*.vim": {"type": "autoload"},
70 | "doc/*.txt": {"type": "doc"},
71 | "README.markdown": {"type": "doc"}
72 | }
73 |
74 | With these in place, you could use `:Eplugin projectionist` to edit
75 | `plugin/projectionist.vim` and `:Edoc projectionist` to edit
76 | `doc/projectionist.txt`. If no argument is given, it will edit an alternate
77 | file of that type (see below) or a projection without a glob. So in this
78 | example `:Edoc` would default to editing `README.markdown`.
79 |
80 | The `E` stands for `edit`. You also get `S`, `V`, and `T` variants that
81 | `split`, `vsplit`, and `tabedit`.
82 |
83 | Tab complete is smart. Not quite "fuzzy finder" smart but smart nonetheless.
84 | (On that note, fuzzy finders are great, but I prefer the navigation command
85 | approach when there are multiple categories of similarly named files.)
86 |
87 | ### Alternate files
88 |
89 | Projectionist provides `:A`, `:AS`, `:AV`, and `:AT` to jump to an "alternate"
90 | file, based on ye olde convention originally established in [a.vim][]. Here's
91 | an example configuration for Maven that allows you to jump between the
92 | implementation and test:
93 |
94 | {
95 | "src/main/java/*.java": {"alternate": "src/test/java/{}.java"},
96 | "src/test/java/*.java": {"alternate": "src/main/java/{}.java"}
97 | }
98 |
99 | In addition, the navigation commands (like `:Eplugin` above) will search
100 | alternates when no argument is given to edit a related file of that type.
101 |
102 | Bonus feature: `:A {filename}` edits a file relative to the root of the
103 | project.
104 |
105 | [a.vim]: http://www.vim.org/scripts/script.php?script_id=31
106 |
107 | ### Buffer configuration
108 |
109 | Check out these examples for a minimal Ruby project:
110 |
111 | {
112 | "*": {"make": "rake"},
113 | "spec/*_spec.rb": {"dispatch": "rspec {file}"}
114 | }
115 |
116 | That second one sets the default for [dispatch.vim][]. Plugins can use
117 | projections for their own configuration.
118 |
119 | [dispatch.vim]: https://github.com/tpope/vim-dispatch
120 |
121 | ## Installation
122 |
123 | Install using your favorite package manager, or use Vim's built-in package
124 | support:
125 |
126 | mkdir -p ~/.vim/pack/tpope/start
127 | cd ~/.vim/pack/tpope/start
128 | git clone https://tpope.io/vim/projectionist.git
129 | vim -u NONE -c "helptags projectionist/doc" -c q
130 |
131 | ## FAQ
132 |
133 | > Why not a clearer filename like `.vim_projections.json`?
134 |
135 | Nothing about the file is Vim specific. See
136 | [projectionist](https://github.com/glittershark/projectionist) for an example
137 | of another tool that uses it.
138 |
139 | ## License
140 |
141 | Copyright © Tim Pope. Distributed under the same terms as Vim itself.
142 | See `:help license`.
143 |
--------------------------------------------------------------------------------
/autoload/projectionist.vim:
--------------------------------------------------------------------------------
1 | " Location: autoload/projectionist.vim
2 | " Author: Tim Pope
3 |
4 | if exists("g:autoloaded_projectionist")
5 | finish
6 | endif
7 | let g:autoloaded_projectionist = 1
8 |
9 | " Section: Utility
10 |
11 | function! s:sub(str, pat, repl) abort
12 | return substitute(a:str, '\v\C'.a:pat, a:repl, '')
13 | endfunction
14 |
15 | function! s:gsub(str, pat, repl) abort
16 | return substitute(a:str, '\v\C'.a:pat, a:repl, 'g')
17 | endfunction
18 |
19 | function! s:startswith(str, prefix) abort
20 | return strpart(a:str, 0, len(a:prefix)) ==# a:prefix
21 | endfunction
22 |
23 | function! s:endswith(str, suffix) abort
24 | return strpart(a:str, len(a:str) - len(a:suffix)) ==# a:suffix
25 | endfunction
26 |
27 | function! s:uniq(list) abort
28 | let i = 0
29 | let seen = {}
30 | while i < len(a:list)
31 | let str = string(a:list[i])
32 | if has_key(seen, str)
33 | call remove(a:list, i)
34 | else
35 | let seen[str] = 1
36 | let i += 1
37 | endif
38 | endwhile
39 | return a:list
40 | endfunction
41 |
42 | function! projectionist#lencmp(i1, i2) abort
43 | return len(a:i1) - len(a:i2)
44 | endfunction
45 |
46 | function! s:real(file) abort
47 | let pre = substitute(matchstr(a:file, '^\a\a\+\ze:'), '^.', '\u&', '')
48 | if empty(pre)
49 | let path = s:absolute(a:file, getcwd())
50 | elseif exists('*' . pre . 'Real')
51 | let path = {pre}Real(a:file)
52 | else
53 | let path = a:file
54 | endif
55 | return exists('+shellslash') && !&shellslash ? tr(path, '/', '\') : path
56 | endfunction
57 |
58 | function! projectionist#slash(...) abort
59 | let s = exists('+shellslash') && !&shellslash ? '\' : '/'
60 | return a:0 ? tr(a:1, '/', s) : s
61 | endfunction
62 |
63 | function! s:slash(str) abort
64 | return exists('+shellslash') ? tr(a:str, '\', '/') : a:str
65 | endfunction
66 |
67 | function! projectionist#json_parse(string) abort
68 | let string = type(a:string) == type([]) ? join(a:string, ' ') : a:string
69 | if exists('*json_decode')
70 | try
71 | return json_decode(string)
72 | catch
73 | endtry
74 | else
75 | let [null, false, true] = ['', 0, 1]
76 | let stripped = substitute(string, '\C"\(\\.\|[^"\\]\)*"', '', 'g')
77 | if stripped !~# "[^,:{}\\[\\]0-9.\\-+Eaeflnr-u \n\r\t]"
78 | try
79 | return eval(substitute(string, "[\r\n]", ' ', 'g'))
80 | catch
81 | endtry
82 | endif
83 | endif
84 | throw "invalid JSON: ".string
85 | endfunction
86 |
87 | function! projectionist#shellescape(arg) abort
88 | return a:arg =~# "^[[:alnum:]_/.:-]\\+$" ? a:arg : shellescape(a:arg)
89 | endfunction
90 |
91 | function! projectionist#shellpath(arg) abort
92 | if empty(a:arg)
93 | return ''
94 | elseif a:arg =~# '^[[:alnum:].+-]\+:'
95 | return projectionist#shellescape(s:real(a:arg))
96 | else
97 | return projectionist#shellescape(a:arg)
98 | endif
99 | endfunction
100 |
101 | function! s:join(arg) abort
102 | if type(a:arg) == type([])
103 | return join(a:arg, ' ')
104 | elseif type(a:arg) == type('')
105 | return a:arg
106 | else
107 | return ''
108 | endif
109 | endfunction
110 |
111 | function! s:parse(mods, args) abort
112 | let flags = ''
113 | let pres = []
114 | let cmd = {'args': []}
115 | if a:mods ==# '' || a:mods ==# ''
116 | let cmd.mods = ''
117 | else
118 | let cmd.mods = a:mods . ' '
119 | endif
120 | let args = copy(a:args)
121 | while !empty(args)
122 | if args[0] =~# "^++mods="
123 | let cmd.mods .= args[0][8:-1] . ' '
124 | elseif args[0] =~# '^++'
125 | let flags .= ' ' . args[0]
126 | elseif args[0] =~# '^+.'
127 | call add(pres, args[0][1:-1])
128 | elseif args[0] !=# '+'
129 | call add(cmd.args, args[0])
130 | endif
131 | call remove(args, 0)
132 | endwhile
133 |
134 | let cmd.pre = flags . (empty(pres) ? '' : ' +'.escape(join(pres, '|'), '| '))
135 | return cmd
136 | endfunction
137 |
138 | function! s:fcall(fn, path, ...) abort
139 | return call(get(get(g:, 'io_' . matchstr(a:path, '^\a\a\+\ze:'), {}), a:fn, a:fn), [a:path] + a:000)
140 | endfunction
141 |
142 | function! s:mkdir_p(path) abort
143 | if a:path !~# '^\a[[:alnum:].+-]\+:' && !isdirectory(a:path)
144 | call mkdir(a:path, 'p')
145 | endif
146 | endfunction
147 |
148 | " Section: Querying
149 |
150 | function! s:roots() abort
151 | return reverse(sort(keys(get(b:, 'projectionist', {})), function('projectionist#lencmp')))
152 | endfunction
153 |
154 | function! projectionist#path(...) abort
155 | let abs = '^/\|^\a\+:\|^\.\.\=\%(/\|$\)'
156 | if a:0 && s:slash(a:1) =~# abs || (a:0 > 1 && a:2 is# 0)
157 | return s:slash(a:1)
158 | endif
159 | if a:0 && type(a:1) ==# type(0)
160 | let root = get(s:roots(), (a:1 < 0 ? -a:1 : a:1) - 1, '')
161 | if a:0 > 1
162 | if s:slash(a:2) =~# abs
163 | return a:2
164 | endif
165 | let file = a:2
166 | endif
167 | elseif a:0 > 1 && type(a:2) == type('')
168 | let root = substitute(s:slash(a:2), '/$', '', '')
169 | let file = a:1
170 | if empty(root)
171 | return file
172 | endif
173 | elseif a:0 == 1 && empty(a:1)
174 | return ''
175 | else
176 | let root = get(s:roots(), a:0 > 1 ? (a:2 < 0 ? -a:2 : a:2) - 1 : 0, '')
177 | if a:0
178 | let file = a:1
179 | endif
180 | endif
181 | if !empty(root) && exists('file')
182 | return root . '/' . file
183 | else
184 | return root
185 | endif
186 | endfunction
187 |
188 | function! s:path(path, ...) abort
189 | if a:0 || type(a:path) == type(0)
190 | return call('projectionist#path', [a:path] + a:000)
191 | else
192 | return a:path
193 | endif
194 | endfunction
195 |
196 | function! projectionist#filereadable(...) abort
197 | return s:fcall('filereadable', call('s:path', a:000))
198 | endfunction
199 |
200 | function! projectionist#isdirectory(...) abort
201 | return s:fcall('isdirectory', call('s:path', a:000))
202 | endfunction
203 |
204 | function! projectionist#getftime(...) abort
205 | return s:fcall('getftime', call('s:path', a:000))
206 | endfunction
207 |
208 | function! projectionist#readfile(path, ...) abort
209 | let args = copy(a:000)
210 | let path = a:path
211 | if s:slash(get(args, 0, '')) =~# '[/.]' || type(get(args, 0, '')) == type(0) || type(path) == type(0)
212 | let path = projectionist#path(path, remove(args, 0))
213 | endif
214 | return call('s:fcall', ['readfile'] + [path] + args)
215 | endfunction
216 |
217 | function! projectionist#glob(file, ...) abort
218 | let root = ''
219 | if a:0
220 | let root = projectionist#path('', a:1)
221 | endif
222 | let path = a:file
223 | if !empty(root) && s:slash(path) !~# '^\.\.\=\%(/\|$\)'
224 | let path = s:absolute(path, root)
225 | endif
226 | let files = s:fcall('glob', path, a:0 > 1 ? a:2 : 0, 1)
227 | if len(root) || a:0 && a:1 is# 0
228 | call map(files, 's:slash(v:val)')
229 | call map(files, 'v:val . (v:val !~# "/$" && projectionist#isdirectory(v:val) ? "/" : "")')
230 | endif
231 | if len(root)
232 | call map(files, 'strpart(v:val, 0, len(root)) ==# root ? strpart(v:val, len(root)) : v:val')
233 | endif
234 | return files
235 | endfunction
236 |
237 | function! projectionist#real(...) abort
238 | return s:real(call('s:path', a:000))
239 | endfunction
240 |
241 | function! s:all() abort
242 | let all = []
243 | for key in s:roots()
244 | for value in b:projectionist[key]
245 | call add(all, [key, value])
246 | endfor
247 | endfor
248 | return all
249 | endfunction
250 |
251 | if !exists('g:projectionist_transformations')
252 | let g:projectionist_transformations = {}
253 | endif
254 |
255 | function! g:projectionist_transformations.dot(input, o) abort
256 | return substitute(a:input, '/', '.', 'g')
257 | endfunction
258 |
259 | function! g:projectionist_transformations.underscore(input, o) abort
260 | return substitute(a:input, '/', '_', 'g')
261 | endfunction
262 |
263 | function! g:projectionist_transformations.backslash(input, o) abort
264 | return substitute(a:input, '/', '\\', 'g')
265 | endfunction
266 |
267 | function! g:projectionist_transformations.colons(input, o) abort
268 | return substitute(a:input, '/', '::', 'g')
269 | endfunction
270 |
271 | function! g:projectionist_transformations.hyphenate(input, o) abort
272 | return tr(a:input, '_', '-')
273 | endfunction
274 |
275 | function! g:projectionist_transformations.blank(input, o) abort
276 | return tr(a:input, '_-', ' ')
277 | endfunction
278 |
279 | function! g:projectionist_transformations.uppercase(input, o) abort
280 | return toupper(a:input)
281 | endfunction
282 |
283 | function! g:projectionist_transformations.camelcase(input, o) abort
284 | return substitute(a:input, '[_-]\(.\)', '\u\1', 'g')
285 | endfunction
286 |
287 | function! g:projectionist_transformations.capitalize(input, o) abort
288 | return substitute(a:input, '\%(^\|/\)\zs\(.\)', '\u\1', 'g')
289 | endfunction
290 |
291 | function! g:projectionist_transformations.snakecase(input, o) abort
292 | let str = a:input
293 | let str = substitute(str, '\v(\u+)(\u\l)', '\1_\2', 'g')
294 | let str = substitute(str, '\v(\l|\d)(\u)', '\1_\2', 'g')
295 | let str = tolower(str)
296 | return str
297 | endfunction
298 |
299 | function! g:projectionist_transformations.dirname(input, o) abort
300 | return a:input !~# '/' ? '.' : substitute(a:input, '/[^/]*$', '', '')
301 | endfunction
302 |
303 | function! g:projectionist_transformations.basename(input, o) abort
304 | return substitute(a:input, '.*/', '', '')
305 | endfunction
306 |
307 | function! g:projectionist_transformations.singular(input, o) abort
308 | let input = a:input
309 | let input = s:sub(input, '%([Mm]ov|[aeio])@ 1 ? a:2 : get(a:0 ? a:1 : {}, 'file', get(b:, 'projectionist_file', expand('%:p')))
426 | for [value, expansions] in projectionist#query_raw(a:key, file)
427 | call extend(expansions, a:0 ? a:1 : {}, 'keep')
428 | call add(candidates, [expansions.project, s:expand_placeholders(value, expansions)])
429 | unlet value
430 | endfor
431 | return candidates
432 | endfunction
433 |
434 | function! s:absolute(path, in) abort
435 | let in_with_slash = a:in . (s:slash(a:in) =~# '/$' ? '' : projectionist#slash())
436 | if s:slash(a:path) =~# '^\%([[:alnum:].+-]\+:\)\|^/\|^$'
437 | return a:path
438 | elseif s:slash(a:path) =~# '^\.\%(/\|$\)'
439 | return in_with_slash[0:-2] . a:path[1 : -1]
440 | else
441 | return in_with_slash . a:path
442 | endif
443 | endfunction
444 |
445 | function! projectionist#query_file(key, ...) abort
446 | let files = []
447 | let _ = {}
448 | for [root, _.match] in projectionist#query(a:key, a:0 ? a:1 : {})
449 | call extend(files, map(filter(type(_.match) == type([]) ? copy(_.match) : [_.match], 'len(v:val)'), 's:absolute(v:val, root)'))
450 | endfor
451 | return s:uniq(files)
452 | endfunction
453 |
454 | let s:projectionist_max_file_recursion = 3
455 |
456 | function! s:query_file_recursive(key, ...) abort
457 | let keys = type(a:key) == type([]) ? a:key : [a:key]
458 | let start_file = get(a:0 ? a:1 : {}, 'file', get(b:, 'projectionist_file', expand('%:p')))
459 | let files = []
460 | let visited_files = {start_file : 1}
461 | let current_files = [start_file]
462 | let depth = 0
463 | while !empty(current_files) && depth < s:projectionist_max_file_recursion
464 | let next_files = []
465 | for file in current_files
466 | let query_opts = extend(a:0 ? copy(a:1) : {}, {'file': file})
467 | for key in keys
468 | let [root, match] = get(projectionist#query(key, query_opts), 0, ['', []])
469 | let subfiles = type(match) == type([]) ? copy(match) : [match]
470 | call map(filter(subfiles, 'len(v:val)'), 's:absolute(v:val, root)')
471 | if !empty(subfiles)
472 | break
473 | endif
474 | endfor
475 | for subfile in subfiles
476 | if !has_key(visited_files, subfile)
477 | let visited_files[subfile] = 1
478 | call add(files, subfile)
479 | call add(next_files, subfile)
480 | endif
481 | endfor
482 | endfor
483 | let current_files = next_files
484 | let depth += 1
485 | endwhile
486 | return files
487 | endfunction
488 |
489 | function! s:shelljoin(val) abort
490 | return substitute(s:join(a:val), '["'']\([{}]\)["'']', '\1', 'g')
491 | endfunction
492 |
493 | function! projectionist#query_exec(key, ...) abort
494 | let opts = extend({'post_function': 'projectionist#shellpath'}, a:0 ? a:1 : {})
495 | return filter(map(projectionist#query(a:key, opts), '[s:real(v:val[0]), s:shelljoin(v:val[1])]'), '!empty(v:val[0]) && !empty(v:val[1])')
496 | endfunction
497 |
498 | function! projectionist#query_scalar(key) abort
499 | let values = []
500 | for [root, match] in projectionist#query(a:key)
501 | if type(match) == type([])
502 | call extend(values, match)
503 | elseif type(match) !=# type({})
504 | call add(values, match)
505 | endif
506 | unlet match
507 | endfor
508 | return values
509 | endfunction
510 |
511 | function! s:query_exec_with_alternate(key) abort
512 | let values = projectionist#query_exec(a:key)
513 | for file in projectionist#query_file('alternate')
514 | for [root, match] in projectionist#query_exec(a:key, {'file': file})
515 | if filereadable(file)
516 | call add(values, [root, match])
517 | endif
518 | unlet match
519 | endfor
520 | endfor
521 | return values
522 | endfunction
523 |
524 | " Section: Activation
525 |
526 | function! projectionist#append(root, ...) abort
527 | if type(a:root) != type('') || empty(a:root)
528 | return
529 | endif
530 | let projections = copy(get(a:000, -1, {}))
531 | if type(projections) == type('') && !empty(projections)
532 | try
533 | let l:.projections = projectionist#json_parse(projectionist#readfile(projections, a:root))
534 | catch
535 | let l:.projections = {}
536 | endtry
537 | endif
538 | if type(projections) == type({})
539 | let root = projectionist#slash(substitute(a:root, '.\zs[' . projectionist#slash() . '/]$', '', ''))
540 | if !has_key(b:projectionist, root)
541 | let b:projectionist[root] = []
542 | endif
543 | for [k, v] in items(filter(copy(projections), 'type(v:val) == type("")'))
544 | if (k =~# '\*') ==# (v =~# '\*') && has_key(projections, v)
545 | let projections[k] = projections[v]
546 | endif
547 | endfor
548 | call add(b:projectionist[root], filter(projections, 'type(v:val) == type({})'))
549 | return 1
550 | endif
551 | endfunction
552 |
553 | function! projectionist#define_navigation_command(command, patterns) abort
554 | for [prefix, excmd] in items(s:prefixes)
555 | execute 'command! -buffer -bar -bang -nargs=* -complete=customlist,s:projection_complete'
556 | \ prefix . substitute(a:command, '\A', '', 'g')
557 | \ ':execute s:open_projection("", "'.excmd.'",'.string(a:patterns).',)'
558 | endfor
559 | endfunction
560 |
561 | function! projectionist#activate() abort
562 | if empty(b:projectionist)
563 | return
564 | endif
565 | if len(s:real(s:roots()[0]))
566 | command! -buffer -bar -bang -nargs=? -range=1 -complete=customlist,s:dir_complete Pcd
567 | \ exe 'cd' projectionist#real(projectionist#path() . '/' . )
568 | command! -buffer -bar -bang -nargs=* -range=1 -complete=customlist,s:dir_complete Ptcd
569 | \ exe (0 ? 'cd' : 'tcd') projectionist#real(projectionist#path() . '/' . )
570 | command! -buffer -bar -bang -nargs=* -range=1 -complete=customlist,s:dir_complete Plcd
571 | \ exe (0 ? 'cd' : 'lcd') projectionist#real(projectionist#path() . '/' . )
572 | if exists(':Cd') != 2
573 | command! -buffer -bar -bang -nargs=? -range=1 -complete=customlist,s:dir_complete Cd
574 | \ exe 'cd' projectionist#real(projectionist#path() . '/' . )
575 | endif
576 | if exists(':Tcd') != 2
577 | command! -buffer -bar -bang -nargs=? -range=1 -complete=customlist,s:dir_complete Tcd
578 | \ exe (0 ? 'cd' : 'tcd') projectionist#real(projectionist#path() . '/' . )
579 | endif
580 | if exists(':Lcd') != 2
581 | command! -buffer -bar -bang -nargs=? -range=1 -complete=customlist,s:dir_complete Lcd
582 | \ echo (0 ? 'cd' : 'lcd') projectionist#real(projectionist#path() . '/' . )
583 | endif
584 | command! -buffer -bang -nargs=1 -range=0 -complete=command ProjectDo
585 | \ exe s:do('', ==?:-1, )
586 | endif
587 | for [command, patterns] in items(projectionist#navigation_commands())
588 | call projectionist#define_navigation_command(command, patterns)
589 | endfor
590 | for [prefix, excmd] in items(s:prefixes) + [['', 'edit']]
591 | execute 'command! -buffer -bar -bang -nargs=* -range=-1 -complete=customlist,s:edit_complete'
592 | \ 'A'.prefix
593 | \ ':execute s:edit_command("", "'.excmd.'", , )'
594 | endfor
595 |
596 | for [root, makeprg] in projectionist#query_exec('make')
597 | unlet! b:current_compiler
598 | let compiler = fnamemodify(matchstr(makeprg, '\S\+'), ':t:r')
599 | setlocal errorformat=%+I%.%#,
600 | if exists(':Dispatch')
601 | silent! let compiler = dispatch#compiler_for_program(makeprg)
602 | endif
603 | if !empty(findfile('compiler/'.compiler.'.vim', escape(&rtp, ' ')))
604 | execute 'compiler' compiler
605 | elseif compiler ==# 'make'
606 | setlocal errorformat<
607 | endif
608 | let &l:makeprg = makeprg
609 | let &l:errorformat .= ',%\&chdir '.escape(root, ',')
610 | break
611 | endfor
612 |
613 | for [root, command] in projectionist#query_exec('console')
614 | let offset = index(s:roots(), root) + 1
615 | let b:start = '++dir=' . fnameescape(root) .
616 | \ ' ++title=' . escape(fnamemodify(root, ':t'), '\ ') . '\ console ' .
617 | \ command
618 | execute 'command! -bar -bang -buffer -nargs=* Console ' .
619 | \ (has('patch-7.4.1898') ? ' ' : '') .
620 | \ (exists(':Start') < 2 ?
621 | \ 'ProjectDo ' . (offset == 1 ? '' : offset.' ') . '!' . command :
622 | \ 'Start ' . b:start) . ' '
623 | break
624 | endfor
625 |
626 | for [root, command] in projectionist#query_exec('start')
627 | let offset = index(s:roots(), root) + 1
628 | let b:start = '++dir=' . fnameescape(root) . ' ' . command
629 | break
630 | endfor
631 |
632 | for [root, command] in s:query_exec_with_alternate('dispatch')
633 | let b:dispatch = '++dir=' . fnameescape(root) . ' ' . command
634 | break
635 | endfor
636 |
637 | for dir in projectionist#query_file('path')
638 | let dir = substitute(dir, '^\a\a\+:', '+&', '')
639 | if stridx(','.&l:path.',', ','.escape(dir, ', ').',') < 0
640 | let &l:path = escape(dir, ', ') . ',' . &path
641 | endif
642 | endfor
643 |
644 | for root in s:roots()
645 | let tags = s:real(root . projectionist#slash() . 'tags')
646 | if len(tags) && stridx(','.&l:tags.',', ','.escape(tags, ', ').',') < 0
647 | let &l:tags = &tags . ',' . escape(tags, ', ')
648 | endif
649 | let outermost = root
650 | endfor
651 | let b:workspace_folder = outermost
652 |
653 | if exists('#User#ProjectionistActivate')
654 | doautocmd User ProjectionistActivate
655 | endif
656 | endfunction
657 |
658 | " Section: Completion
659 |
660 | function! projectionist#completion_filter(results, query, sep, ...) abort
661 | if a:query =~# '\*'
662 | let regex = s:gsub(a:query, '\*', '.*')
663 | return filter(copy(a:results),'v:val =~# "^".regex')
664 | endif
665 |
666 | let C = get(g:, 'projectionist_completion_filter', get(g:, 'completion_filter'))
667 | if type(C) == type({}) && has_key(C, 'Apply')
668 | let results = call(C.Apply, [a:results, a:query, a:sep, a:0 ? a:1 : {}], C)
669 | elseif type(C) == type('') && exists('*'.C)
670 | let results = call(C, [a:results, a:query, a:sep, a:0 ? a:1 : {}])
671 | endif
672 | if get(l:, 'results') isnot# 0
673 | return results
674 | endif
675 | unlet! results
676 |
677 | let results = s:uniq(sort(copy(a:results)))
678 | call filter(results,'v:val !~# "\\~$" && !empty(v:val)')
679 | let filtered = filter(copy(results),'v:val[0:strlen(a:query)-1] ==# a:query')
680 | if !empty(filtered) | return filtered | endif
681 | if !empty(a:sep)
682 | let regex = s:gsub(a:query,'[^'.a:sep.']','[&].*')
683 | let filtered = filter(copy(results),'v:val =~# "^".regex')
684 | if !empty(filtered) | return filtered | endif
685 | let filtered = filter(copy(results),'a:sep.v:val =~# ''['.a:sep.']''.regex')
686 | if !empty(filtered) | return filtered | endif
687 | endif
688 | let regex = s:gsub(a:query,'.','[&].*')
689 | let filtered = filter(copy(results),'v:val =~# regex')
690 | return filtered
691 | endfunction
692 |
693 | function! s:dir_complete(lead, cmdline, _) abort
694 | let pattern = substitute(a:lead, '^\@!\%(^\a\+:/*\)\@ 0
848 | let expansions.lnum = a:count
849 | endif
850 | let alternates = projectionist#query_file('alternate', expansions)
851 | let warning = get(filter(copy(alternates), 'v:val =~# "replace %.*}"'), 0, '')
852 | if !empty(warning)
853 | return 'echoerr '.string(matchstr(warning, 'replace %.*}').' in alternate projection')
854 | endif
855 | call map(alternates, 's:jumpopt(v:val)')
856 | let open = get(filter(copy(alternates), 'projectionist#getftime(v:val[0]) >= 0'), 0, [])
857 | if empty(alternates)
858 | return 'echoerr "No alternate file"'
859 | elseif empty(open)
860 | let choices = ['Create alternate file?']
861 | let i = 0
862 | for [alt, _] in alternates
863 | let i += 1
864 | call add(choices, i . ' ' . fnamemodify(alt, ':~:.'))
865 | endfor
866 | let i = inputlist(choices)
867 | if i > 0
868 | let open = get(alternates, i-1, [])
869 | endif
870 | if empty(open)
871 | return ''
872 | endif
873 | endif
874 | endif
875 | let [file, jump] = open
876 | call s:mkdir_p(fnamemodify(file, ':h'))
877 | return cmd.mods . a:edit . cmd.pre . ' ' .
878 | \ jump . fnameescape(fnamemodify(file, ':~:.'))
879 | endfunction
880 |
881 | function! s:edit_complete(lead, cmdline, _) abort
882 | let pattern = substitute(a:lead, '^\@!\%(^\a\+:/*\)\@= 0 ? a:count : '').substitute(cmd, '\>', a:bang, '')
898 | catch
899 | return 'echoerr '.string(v:exception)
900 | finally
901 | execute cd fnameescape(cwd)
902 | endtry
903 | return ''
904 | endfunction
905 |
906 | " Section: Make
907 |
908 | function! s:qf_pre() abort
909 | let dir = substitute(matchstr(','.&l:errorformat, ',\%(%\\&\)\=\%(ch\)\=dir[ =]\zs\%(\\.\|[^,]\)*'), '\\,' ,',', 'g')
910 | let cwd = getcwd()
911 | if !empty(dir) && dir !=# cwd
912 | let cd = haslocaldir() ? 'lcd' : exists(':tcd') && haslocaldir(-1) ? 'tcd' : 'cd'
913 | execute cd fnameescape(dir)
914 | let s:qf_post = cd . ' ' . fnameescape(cwd)
915 | endif
916 | endfunction
917 |
918 | augroup projectionist_make
919 | autocmd!
920 | autocmd QuickFixCmdPre *make* call s:qf_pre()
921 | autocmd QuickFixCmdPost *make*
922 | \ if exists('s:qf_post') | execute remove(s:, 'qf_post') | endif
923 | augroup END
924 |
925 | " Section: Templates
926 |
927 | function! projectionist#apply_template() abort
928 | if !&modifiable
929 | return ''
930 | endif
931 | let template = get(projectionist#query('template'), 0, ['', ''])[1]
932 | if type(template) == type([]) && type(get(template, 0)) == type([])
933 | let template = template[0]
934 | endif
935 | if type(template) == type([])
936 | let l:.template = join(template, "\n")
937 | endif
938 | if !empty(template)
939 | silent %delete_
940 | if template =~# '\t' && !exists('b:sleuth') && exists(':Sleuth') == 2
941 | silent! Sleuth!
942 | endif
943 | if exists('#User#ProjectionistApplyTemplatePre')
944 | doautocmd User ProjectionistApplyTemplatePre
945 | endif
946 | if &et
947 | let template = s:gsub(template, '\t', repeat(' ', &sw ? &sw : &ts))
948 | endif
949 | call setline(1, split(template, "\n"))
950 | if exists('#User#ProjectionistApplyTemplate')
951 | doautocmd User ProjectionistApplyTemplate
952 | endif
953 | doautocmd BufReadPost
954 | endif
955 | return ''
956 | endfunction
957 |
--------------------------------------------------------------------------------
/doc/projectionist.txt:
--------------------------------------------------------------------------------
1 | *projectionist.txt* *projectionist* Project configuration
2 |
3 | Author: Tim Pope
4 | Repo: https://github.com/tpope/vim-projectionist
5 | License: Same terms as Vim itself (see |license|)
6 |
7 | SETUP *projectionist-setup*
8 |
9 | Projections are maps from file names and globs to sets of properties
10 | describing the file. The simplest way to define them is to create a
11 | ".projections.json" in the root of the project. Here's a simple example for a
12 | Maven project:
13 | >
14 | {
15 | "src/main/java/*.java": {
16 | "alternate": "src/test/java/{}.java",
17 | "type": "source"
18 | },
19 | "src/test/java/*.java": {
20 | "alternate": "src/main/java/{}.java",
21 | "type": "test"
22 | },
23 | "*.java": {"dispatch": "javac {file}"},
24 | "*": {"make": "mvn"}
25 | }
26 | <
27 | In property values, "{}" will be replaced by the portion of the glob matched
28 | by the "*". You can also chain one or more transformations inside the braces
29 | separated by bars, e.g. "{dot|hyphenate}". The complete list of available
30 | transformations is as follows:
31 |
32 | Name Behavior ~
33 | dot / to .
34 | underscore / to _
35 | backslash / to \
36 | colons / to ::
37 | hyphenate _ to -
38 | blank _ and - to space
39 | uppercase uppercase
40 | camelcase foo_bar/baz_quux to fooBar/bazQuux
41 | snakecase FooBar/bazQuux to foo_bar/baz_quux
42 | capitalize capitalize first letter and each letter after a slash
43 | dirname remove last slash separated component
44 | basename remove all but last slash separated component
45 | singular singularize
46 | plural pluralize
47 | file absolute path to file
48 | project absolute path to project
49 | open literal {
50 | close literal }
51 | nothing empty string
52 | vim no-op (include to specify other implementations should ignore)
53 |
54 | From a globbing perspective, "*" is actually a stand in for "**/*". For
55 | advanced cases, you can include both globs explicitly: "test/**/test_*.rb".
56 | When expanding with {}, the ** and * portions are joined with a slash. If
57 | necessary, the dirname and basename expansions can be used to split the value
58 | back apart.
59 |
60 | The full list of available properties in a projection is as follows:
61 |
62 | *projectionist-alternate*
63 | "alternate" ~
64 | Determines the destination of the |projectionist-:A| command. If this
65 | is a list, the first readable file will be used. Will also be used as
66 | a default for |projectionist-related|.
67 | *projectionist-console*
68 | "console" ~
69 | Command to run to start a REPL or other interactive shell. Will be
70 | defined as :Console. This is useful to set from a "*" projection or
71 | on a simple file glob like "*.js". Will also be used as a default for
72 | "start". Expansions are shell escaped.
73 | *projectionist-dispatch*
74 | "dispatch" ~
75 | Default task to use for |:Dispatch| in dispatch.vim. If not provided,
76 | the option for any existing alternate file is used instead.
77 | Expansions are shell escaped.
78 | *projectionist-make*
79 | "make" ~
80 | Sets 'makeprg'. Also loads a |:compiler| plugin if one is available
81 | matching the executable name. This is useful to set from a "*"
82 | projection. Expansions are shell escaped.
83 | *projectionist-path*
84 | "path" ~
85 | Additional directories to prepend to 'path'. Can be relative to the
86 | project root or absolute. This is useful to set on a simple file glob
87 | like "*.js".
88 | *projectionist-related*
89 | "related" ~
90 | Indicates one or more files to search when a navigation command is
91 | called without an argument, to find a default destination. Related
92 | files are searched recursively.
93 | *projectionist-start*
94 | "start" ~
95 | Command to run to "boot" the project. Examples include `lein run`,
96 | `rails server`, and `foreman start`. It will be used as a default
97 | task for |:Start| in dispatch.vim. This is useful to set from a "*"
98 | projection. Expansions are shell escaped.
99 | *projectionist-template*
100 | "template" ~
101 | Array of lines to use when creating a new file.
102 | *projectionist-type*
103 | "type" ~
104 | Declares the type of file and create a set of navigation commands for
105 | opening files that match the glob. If this option is provided for a
106 | literal filename rather than a glob, it is used as the default
107 | destination of the navigation command when no argument is given. For
108 | a "type" value of "foo", the following navigation commands will be
109 | provided:
110 | `:Efoo`: delegates to |:edit|
111 | `:Sfoo`: delegates to |:split|
112 | `:Vfoo`: delegates to |:vsplit|
113 | `:Tfoo`: delegates to |:tabedit|
114 | `:Dfoo`: delegates to |:read|
115 | `:Ofoo`: delegates to |:drop|
116 |
117 | *g:projectionist_heuristics*
118 | In addition to ".projections.json", projections can be defined globally
119 | through use of the |projectionist-autocmds| API or through the variable
120 | g:projectionist_heuristics, a |Dictionary| mapping between a string describing
121 | the root of the project and a set of projections. The keys of the dictionary
122 | are files and directories that can be found in the root of a project, with &
123 | separating multiple requirements and | separating multiple alternatives. You
124 | can also prefix a file or directory with ! to forbid rather than require its
125 | presence.
126 |
127 | In the example below, the first key requires a directory named
128 | lib/heroku/ and a file named init.rb, and the second requires a directory
129 | named etc/rbenv.d/ or one or more files matching the glob bin/rbenv-*.
130 | >
131 | let g:projectionist_heuristics = {
132 | \ "lib/heroku/&init.rb": {
133 | \ "lib/heroku/command/*.rb": {"type": "command"}
134 | \ },
135 | \ "etc/rbenv.d/|bin/rbenv-*": {
136 | \ "bin/rbenv-*": {"type": "command"},
137 | \ "etc/rbenv.d/*.bash": {"type": "hook"}
138 | \ }}
139 |
140 | Note the use of VimScript |line-continuation|.
141 |
142 | COMMANDS *projectionist-commands*
143 |
144 | In addition to any navigation commands provided by your projections (see
145 | |projectionist-type|), the following commands are available.
146 |
147 | *projectionist-:A*
148 | :A Edit the alternate file for the current buffer, as
149 | defined by the "alternate" key.
150 |
151 | :A {file} Edit {file} relative to the innermost root.
152 | *projectionist-:AS*
153 | :AS [file] Like :A, but open in a split.
154 | *projectionist-:AV*
155 | :AV [file] Like :A, but open in a vertical split.
156 | *projectionist-:AT*
157 | :AT [file] Like :A, but open in a tab.
158 | *projectionist-:AO*
159 | :AO [file] Like :A, but open using |:drop|.
160 |
161 | *projectionist-:AD*
162 | :AD Replace the contents of the buffer with the new file
163 | template.
164 |
165 | :AD {file} Like :A, but |:read| the file into the current buffer.
166 |
167 | *projectionist-:Pcd*
168 | *projectionist-:Cd*
169 | :Pcd |:cd| to the innermost root.
170 |
171 | :Pcd {path} |:cd| to {path} in the innermost root.
172 |
173 | *projectionist-:Plcd*
174 | *projectionist-:Lcd*
175 | :Plcd [path] Like :Pcd, but use |:lcd|.
176 |
177 | *projectionist-:Ptcd*
178 | *projectionist-:Tcd*
179 | :Ptcd [path] Like :Pcd, but use |:tcd|.
180 |
181 | *projectionist-:ProjectDo*
182 | :ProjectDo {cmd} Change directory to the project root, execute the
183 | given command, and change back. This won't work if
184 | {cmd} leaves the focus in a different window. Tab
185 | complete will erroneously reflect the current working
186 | directory, not the project root.
187 |
188 | AUTOCOMMANDS *projectionist-autocmds*
189 |
190 | Projectionist.vim dispatches a |User| *ProjectionistDetect* event when
191 | searching for projections for a buffer. You can call *projectionist#append()*
192 | to register projections for the file found in *g:projectionist_file* .
193 | >
194 | autocmd User ProjectionistDetect
195 | \ if SomeCondition(g:projectionist_file) |
196 | \ call projectionist#append(root, projections) |
197 | \ endif
198 | <
199 | The |User| *ProjectionistActivate* event is triggered when one or more sets of
200 | projections are found. You can call *projectionist#query()* to retrieve an
201 | array of pairs of project roots and values for a given key. Since typically
202 | you only care about the first (most precisely targeted) value, the following
203 | pattern may prove useful:
204 | >
205 | autocmd User ProjectionistActivate call s:activate()
206 |
207 | function! s:activate() abort
208 | for [root, value] in projectionist#query('wrap')
209 | let &l:textwidth = value
210 | break
211 | endfor
212 | endfunction
213 | <
214 | You can also call *projectionist#path()* to get the root of the innermost set
215 | of projections, which is useful for implementing commands like
216 | |projectionist-:Pcd|.
217 |
218 | vim:tw=78:et:ft=help:norl:
219 |
--------------------------------------------------------------------------------
/plugin/projectionist.vim:
--------------------------------------------------------------------------------
1 | " Location: plugin/projectionist.vim
2 | " Author: Tim Pope
3 | " Version: 1.3
4 | " GetLatestVimScripts: 4989 1 :AutoInstall: projectionist.vim
5 |
6 | if exists("g:loaded_projectionist") || v:version < 704 || &cp
7 | finish
8 | endif
9 | let g:loaded_projectionist = 1
10 |
11 | " ProjectionistHas('Gemfile&lib/|*.gemspec', '/path/to/root')
12 | function! ProjectionistHas(req, ...) abort
13 | if type(a:req) != type('')
14 | return
15 | endif
16 | let ns = matchstr(a:0 ? a:1 : a:req, '^\a\a\+\ze:')
17 | if !a:0
18 | return s:nscall(ns, a:req =~# '[\/]$' ? 'isdirectory' : 'filereadable', a:req)
19 | endif
20 | for test in split(a:req, '|')
21 | if s:has(ns, a:1, test)
22 | return 1
23 | endif
24 | endfor
25 | endfunction
26 |
27 | let s:slash = exists('+shellslash') ? '\' : '/'
28 |
29 | if !exists('g:projectionist_heuristics')
30 | let g:projectionist_heuristics = {}
31 | endif
32 |
33 | function! s:nscall(ns, fn, path, ...) abort
34 | if !get(g:, 'projectionist_ignore_' . a:ns)
35 | return call(get(get(g:, 'io_' . a:ns, {}), a:fn, a:fn), [a:path] + a:000)
36 | else
37 | return call(a:fn, [a:path] + a:000)
38 | endif
39 | endfunction
40 |
41 | function! s:has(ns, root, requirements) abort
42 | if empty(a:requirements)
43 | return 0
44 | endif
45 | for test in split(a:requirements, '&')
46 | let relative = '/' . matchstr(test, '[^!/].*')
47 | if relative =~# '\*'
48 | let found = !empty(s:nscall(a:ns, 'glob', escape(a:root, '[?*') . relative))
49 | elseif relative =~# '/$'
50 | let found = s:nscall(a:ns, 'isdirectory', a:root . relative)
51 | else
52 | let found = s:nscall(a:ns, 'filereadable', a:root . relative)
53 | endif
54 | if test =~# '^!' ? found : !found
55 | return 0
56 | endif
57 | endfor
58 | return 1
59 | endfunction
60 |
61 | function! s:IsAbs(path) abort
62 | return tr(a:path, s:slash, '/') =~# '^/\|^\a\+:'
63 | endfunction
64 |
65 | function! s:Detect(...) abort
66 | let b:projectionist = {}
67 | unlet! b:projectionist_file
68 | if a:0
69 | let file = a:1
70 | elseif &l:buftype =~# '^\%(nowrite\)\=$' && len(@%) || &l:buftype =~# '^\%(nofile\|acwrite\)' && s:IsAbs(@%)
71 | let file = @%
72 | else
73 | return
74 | endif
75 | if !s:IsAbs(file)
76 | let s = exists('+shellslash') && !&shellslash ? '\' : '/'
77 | let file = substitute(getcwd(), '\' . s . '\=$', s, '') . file
78 | endif
79 | let file = substitute(file, '[' . s:slash . '/]$', '', '')
80 |
81 | try
82 | if exists('*ExcludeBufferFromDiscovery') && ExcludeBufferFromDiscovery(file, 'projectionist')
83 | return
84 | endif
85 | catch
86 | endtry
87 | let ns = matchstr(file, '^\a\a\+\ze:')
88 | if empty(ns)
89 | let file = resolve(file)
90 | elseif get(g:, 'projectionist_ignore_' . ns)
91 | return
92 | endif
93 | let root = file
94 | if empty(ns) && !isdirectory(root)
95 | let root = fnamemodify(root, ':h')
96 | endif
97 | let previous = ""
98 | while root !=# previous && root !~# '^\.\=$\|^[\/][\/][^\/]*$'
99 | if s:nscall(ns, 'filereadable', root . '/.projections.json')
100 | try
101 | let value = projectionist#json_parse(projectionist#readfile(root . '/.projections.json'))
102 | call projectionist#append(root, value)
103 | catch /^invalid JSON:/
104 | endtry
105 | endif
106 | for [key, value] in items(g:projectionist_heuristics)
107 | for test in split(key, '|')
108 | if s:has(ns, root, test)
109 | call projectionist#append(root, value)
110 | break
111 | endif
112 | endfor
113 | endfor
114 | let previous = root
115 | let root = fnamemodify(root, ':h')
116 | endwhile
117 |
118 | if exists('#User#ProjectionistDetect')
119 | try
120 | let g:projectionist_file = file
121 | doautocmd User ProjectionistDetect
122 | finally
123 | unlet! g:projectionist_file
124 | endtry
125 | endif
126 |
127 | if !empty(b:projectionist)
128 | let b:projectionist_file = file
129 | call projectionist#activate()
130 | endif
131 | endfunction
132 |
133 | if !exists('g:did_load_ftplugin')
134 | filetype plugin on
135 | endif
136 |
137 | augroup projectionist
138 | autocmd!
139 | autocmd FileType *
140 | \ if &filetype !=# 'netrw' |
141 | \ call s:Detect() |
142 | \ elseif !exists('b:projectionist') |
143 | \ call s:Detect(get(b:, 'netrw_curdir', @%)) |
144 | \ endif
145 | autocmd BufFilePost *
146 | \ if type(getbufvar(+expand(''), 'projectionist')) == type({}) |
147 | \ call s:Detect(expand('')) |
148 | \ endif
149 | autocmd BufNewFile,BufReadPost *
150 | \ if empty(&filetype) |
151 | \ call s:Detect() |
152 | \ endif
153 | autocmd CmdWinEnter *
154 | \ if !empty(getbufvar('#', 'projectionist_file')) |
155 | \ let b:projectionist_file = getbufvar('#', 'projectionist_file') |
156 | \ let b:projectionist = getbufvar('#', 'projectionist') |
157 | \ call projectionist#activate() |
158 | \ endif
159 | autocmd User NERDTreeInit,NERDTreeNewRoot
160 | \ if exists('b:NERDTree.root.path.str') |
161 | \ call s:Detect(b:NERDTree.root.path.str()) |
162 | \ endif
163 | autocmd VimEnter *
164 | \ if get(g:, 'projectionist_vim_enter', 1) && argc() == 0 && empty(v:this_session) |
165 | \ call s:Detect(getcwd()) |
166 | \ endif
167 | autocmd BufWritePost .projections.json call s:Detect(expand(''))
168 | autocmd BufNewFile *
169 | \ if !empty(get(b:, 'projectionist')) |
170 | \ call projectionist#apply_template() |
171 | \ endif
172 | augroup END
173 |
--------------------------------------------------------------------------------