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