├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── README.md ├── Rakefile ├── VimFlavor ├── VimFlavor.lock ├── addon-info.json ├── autoload ├── composer.vim └── composer │ ├── autoload.vim │ ├── commandline.vim │ ├── namespace.vim │ └── semver.vim ├── doc └── composer.txt ├── plugin └── composer.vim └── t ├── activation.vim ├── completion.vim ├── fixtures ├── project-composer │ ├── composer.json │ ├── index.php │ ├── src │ │ └── index.php │ └── vendor │ │ ├── autoload.php │ │ └── foo │ │ └── bar │ │ ├── composer.json │ │ └── index.php ├── project-other │ └── index.php ├── project-phar │ ├── composer.json │ ├── composer.phar │ ├── index.php │ └── vendor │ │ ├── autoload.php │ │ └── foo │ │ └── bar │ │ ├── composer.json │ │ └── index.php └── project-uninstalled │ ├── composer.json │ ├── index.php │ └── src │ └── index.php ├── namespace.vim ├── project.vim ├── semver.vim └── utils.vim /.gitignore: -------------------------------------------------------------------------------- 1 | .vim-flavor 2 | 3 | # Packaged plug-in dir 4 | pkg/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without development 3 | rvm: 4 | - 2.2.1 5 | script: rake ci 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Testing 4 | 5 | Tests are written for [vspec][vspec], which can be installed via 6 | [vim-flavor][vim-flavor]: 7 | 8 | bundle install 9 | bundle exec vim-flavor install 10 | 11 | The test suite can then be run via the rake task: 12 | 13 | bundle exec rake test 14 | 15 | ## Documentation 16 | 17 | The documentation in `doc/` is generated from the plug-in source code via 18 | [vimdoc][vimdoc]. Do not edit `doc/.txt` directly. Refer to the 19 | existing inline documentation as a guide for documenting new code. 20 | 21 | The help doc can be rebuilt by running: 22 | 23 | bundle exec rake doc 24 | 25 | ## Automation 26 | 27 | If you wish, you may use the provided `Guardfile` to automatically run tests 28 | and rebuild the documentation as you make changes: 29 | 30 | bundle exec guard start 31 | 32 | [vspec]: https://github.com/kana/vim-vspec 33 | [vim-flavor]: https://github.com/kana/vim-flavor 34 | [vimdoc]: https://github.com/google/vimdoc 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development do 4 | gem 'guard-rake' 5 | end 6 | 7 | group :test do 8 | gem 'vim-flavor', '~> 2.2' 9 | gem 'rake' 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | blankslate (3.1.3) 5 | coderay (1.1.0) 6 | ffi (1.12.2) 7 | formatador (0.2.5) 8 | guard (2.13.0) 9 | formatador (>= 0.2.4) 10 | listen (>= 2.7, <= 4.0) 11 | lumberjack (~> 1.0) 12 | nenv (~> 0.1) 13 | notiffany (~> 0.0) 14 | pry (>= 0.9.12) 15 | shellany (~> 0.0) 16 | thor (>= 0.18.1) 17 | guard-rake (1.0.0) 18 | guard 19 | rake 20 | listen (3.0.3) 21 | rb-fsevent (>= 0.9.3) 22 | rb-inotify (>= 0.9) 23 | lumberjack (1.0.9) 24 | method_source (0.8.2) 25 | nenv (0.2.0) 26 | notiffany (0.0.7) 27 | nenv (~> 0.1) 28 | shellany (~> 0.0) 29 | parslet (1.7.0) 30 | blankslate (>= 2.0, <= 4.0) 31 | pry (0.10.1) 32 | coderay (~> 1.1.0) 33 | method_source (~> 0.8.1) 34 | slop (~> 3.4) 35 | rake (12.3.3) 36 | rb-fsevent (0.9.5) 37 | rb-inotify (0.9.5) 38 | ffi (>= 0.5.0) 39 | shellany (0.0.1) 40 | slop (3.6.0) 41 | thor (0.19.1) 42 | vim-flavor (2.2.1) 43 | parslet (~> 1.7) 44 | thor (~> 0.19) 45 | 46 | PLATFORMS 47 | ruby 48 | 49 | DEPENDENCIES 50 | guard-rake 51 | rake 52 | vim-flavor (~> 2.2) 53 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rake', :task => 'test' do 2 | watch(%r{^(autoload|plugin|t)/.*\.vim$}) 3 | end 4 | 5 | guard 'rake', :task => 'doc' do 6 | watch(%r{^(autoload|plugin)/.*\.vim$}) 7 | watch(%r{^addon-info\.json$}) 8 | end 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-composer 2 | 3 | Vim support for [Composer PHP][composer] projects. 4 | 5 | [![Build Status][buildimg]](https://travis-ci.org/noahfrederick/vim-composer) 6 | [![Release][release]](https://github.com/noahfrederick/vim-composer/releases) 7 | 8 | [composer]: https://getcomposer.org/ 9 | [buildimg]: https://img.shields.io/travis/noahfrederick/vim-composer/master.svg 10 | [release]: https://img.shields.io/github/tag/noahfrederick/vim-composer.svg?maxAge=2592000 11 | 12 | ## Features 13 | 14 | Composer.vim provides conveniences for working with Composer PHP projects. 15 | Some features include: 16 | 17 | * `:Composer` command wrapper around `composer` with smart completion 18 | * Navigate to source files using Composer's autoloader 19 | * Insert `use` statement for the class/interface/trait under cursor 20 | * [Projectionist][projectionist] support (e.g., `:Ecomposer` to edit your 21 | `composer.json`, `:A` to jump to `composer.lock` and back) 22 | * [Dispatch][dispatch] support (`:Dispatch` runs `composer dump-autoload`) 23 | 24 | See `:help composer` for details. 25 | 26 | ## Installation and Requirements 27 | 28 | Using vim-plug, for example: 29 | 30 | Plug 'noahfrederick/vim-composer' 31 | 32 | Optionally install [Dispatch.vim][dispatch] and 33 | [Projectionist.vim][projectionist] for projections and asynchronous command 34 | execution: 35 | 36 | Plug 'tpope/vim-dispatch' 37 | Plug 'tpope/vim-projectionist' 38 | 39 | **Note**: either Projectionist.vim or Vim version 7.4.1304 or later is required 40 | for JSON support. 41 | 42 | ## Credits and License 43 | 44 | Thanks to Tim Pope for [Bundler.vim][bundler] on which Composer.vim is modeled. 45 | 46 | Copyright © Noah Frederick. Distributed under the same terms as Vim itself. 47 | See `:help license`. 48 | 49 | [projectionist]: https://github.com/tpope/vim-projectionist 50 | [dispatch]: https://github.com/tpope/vim-dispatch 51 | [bundler]: https://github.com/tpope/vim-bundler 52 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | require 'rake/packagetask' 4 | require 'json' 5 | 6 | plugin = JSON.load(File.new('addon-info.json')) 7 | 8 | desc 'Target for CI server' 9 | task ci: [:dump, :test] 10 | 11 | desc 'Dump Vim\'s version info' 12 | task :dump do 13 | sh 'vim --version' 14 | end 15 | 16 | desc 'Run tests with vspec' 17 | task :test do 18 | sh 'bundle exec vim-flavor test' 19 | end 20 | 21 | desc 'Rebuild the documentation with vimdoc' 22 | task :doc do 23 | sh 'vimdoc ./' 24 | end 25 | 26 | Rake::PackageTask.new(plugin['name']) do |p| 27 | p.version = plugin['version'] 28 | p.need_zip = true 29 | p.package_files.include(['plugin/*.vim', 'autoload/*.vim', 'doc/*.txt']) 30 | end 31 | -------------------------------------------------------------------------------- /VimFlavor: -------------------------------------------------------------------------------- 1 | flavor 'kana/vim-vspec', '~> 1.6' 2 | -------------------------------------------------------------------------------- /VimFlavor.lock: -------------------------------------------------------------------------------- 1 | kana/vim-vspec (1.6.1) 2 | -------------------------------------------------------------------------------- /addon-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "composer", 3 | "version": "1.2.0", 4 | "author": "Noah Frederick", 5 | "description": "Vim support for Composer PHP projects", 6 | "homepage": "https://github.com/noahfrederick/vim-composer", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/noahfrederick/vim-composer" 10 | }, 11 | "dependencies": { 12 | "projectionist": { 13 | "type": "git", 14 | "url": "git://github.com/tpope/vim-projectionist" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /autoload/composer.vim: -------------------------------------------------------------------------------- 1 | " autoload/composer.vim - Composer autoloads 2 | " Maintainer: Noah Frederick 3 | 4 | "" 5 | " Throw error with {msg} and replacements. 6 | function! s:throw(...) abort 7 | let msg = a:0 > 1 ? call('printf', a:000) : a:1 8 | let v:errmsg = 'composer: ' . msg 9 | throw v:errmsg 10 | endfunction 11 | 12 | "" 13 | " Like @function(get) but allows for querying nested keys. The {key} may 14 | " contain a dot separator to delimit nested keys: 15 | " 16 | " let val = s:get_nested(dict, 'foo.bar') 17 | " 18 | function! s:get_nested(dict, key, ...) abort 19 | let parts = split(a:key, '\.') 20 | let dict = a:dict 21 | let default = get(a:000, 0, '') 22 | 23 | for part in parts 24 | unlet! val 25 | let val = get(dict, part, 'x-undefined') 26 | 27 | if type(val) == type('') && val ==# 'x-undefined' 28 | return default 29 | elseif type(val) == type({}) 30 | let dict = val 31 | endif 32 | endfor 33 | 34 | return val 35 | endfunction 36 | 37 | "" 38 | " Get Dict from JSON {expr}. 39 | function! s:json_decode(expr) abort 40 | try 41 | if exists('*json_decode') 42 | let expr = type(a:expr) == type([]) ? join(a:expr, "\n") : a:expr 43 | return json_decode(expr) 44 | else 45 | return projectionist#json_parse(a:expr) 46 | endif 47 | catch /^Vim\%((\a\+)\)\=:E474/ 48 | call s:throw('composer.json cannot be parsed') 49 | catch /^invalid JSON/ 50 | call s:throw('composer.json cannot be parsed') 51 | catch /^Vim\%((\a\+)\)\=:E117/ 52 | call s:throw('projectionist is not available') 53 | endtry 54 | return {} 55 | endfunction 56 | 57 | "" 58 | " Get Funcref from script-local function {name}. 59 | function! s:function(name) abort 60 | let func_name = split(expand(''), '\.\.')[-1] 61 | return function(substitute(a:name, '^s:', matchstr(func_name, '\d\+_'), '')) 62 | endfunction 63 | 64 | "" 65 | " Add {method_names} to prototype {namespace} Dict. Follows the same pattern 66 | " as rake.vim. 67 | function! s:add_methods(namespace, method_names) abort 68 | for name in a:method_names 69 | let s:{a:namespace}_prototype[name] = s:function('s:' . a:namespace . '_' . name) 70 | endfor 71 | endfunction 72 | 73 | let s:project_prototype = {} 74 | let s:projects = {} 75 | 76 | "" 77 | " @private 78 | " Get the project object belonging to the current project root, or that 79 | " of [root]. Initializes the project if not initialized. 80 | function! composer#project(...) abort 81 | let root = get(a:000, 0, exists('b:composer_root') && b:composer_root !=# '' ? b:composer_root : '') 82 | 83 | if empty(root) 84 | return {} 85 | endif 86 | 87 | if !has_key(s:projects, root) 88 | let s:projects[root] = deepcopy(s:project_prototype) 89 | let s:projects[root]._root = root 90 | endif 91 | 92 | return get(s:projects, root, {}) 93 | endfunction 94 | 95 | "" 96 | " Get the project object belonging to the current project root, or that 97 | " of [root]. Throws an error if not in a composer project. 98 | function! s:project(...) abort 99 | let project = call('composer#project', a:000) 100 | 101 | if !empty(project) 102 | return project 103 | endif 104 | 105 | call s:throw('%s does not belong to a composer project', expand('%:p')) 106 | endfunction 107 | 108 | "" 109 | " Get absolute path to project root, optionally with [path] appended. 110 | function! s:project_path(...) dict abort 111 | return join([self._root] + a:000, '/') 112 | endfunction 113 | 114 | "" 115 | " Get vendor directory, optionally with [path] appended. 116 | function! s:project_vendor_dir(...) dict abort 117 | let dir = self.query('config.vendor-dir', 'vendor') 118 | 119 | if dir[0] !=# '/' 120 | let dir = call('s:project_path', [dir] + a:000, self) 121 | endif 122 | 123 | return dir 124 | endfunction 125 | 126 | "" 127 | " Check whether file is readable in project. 128 | function! s:project_has_file(file) dict abort 129 | let path = a:file[0] ==# '/' ? a:file : self.path(a:file) 130 | return filereadable(path) 131 | endfunction 132 | 133 | "" 134 | " Change working directory to project root or [dir], respecting current 135 | " window's local dir state. Returns old working directory to be restored later 136 | " by a second invocation of the function. 137 | function! s:project_cd(...) dict abort 138 | let dir = get(a:000, 0, self.path()) 139 | let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' 140 | let cwd = getcwd() 141 | execute cd fnameescape(dir) 142 | return cwd 143 | endfunction 144 | 145 | call s:add_methods('project', ['path', 'vendor_dir', 'has_file', 'cd']) 146 | 147 | "" 148 | " Get JSON contents of composer.json as a Dict. 149 | function! s:project_json() dict abort 150 | if self.cache.needs('json') 151 | call self.cache.set('json', s:json_decode(readfile(self.path('composer.json')))) 152 | endif 153 | 154 | return self.cache.get('json') 155 | endfunction 156 | 157 | "" 158 | " Get JSON contents of composer.lock as a Dict. 159 | function! s:project_lock() dict abort 160 | if self.cache.needs('lock') && self.has_file('composer.lock') 161 | call self.cache.set('lock', s:json_decode(readfile(self.path('composer.lock')))) 162 | endif 163 | 164 | return self.cache.get('lock') 165 | endfunction 166 | 167 | "" 168 | " Get JSON contents of installed.json as a Dict. 169 | function! s:project_installed_json() dict abort 170 | if self.cache.needs('installed_json') 171 | if self.has_file('vendor/composer/installed.json') 172 | call self.cache.set('installed_json', s:json_decode(readfile(self.path('vendor/composer/installed.json')))) 173 | else 174 | call self.cache.set('installed_json', []) 175 | endif 176 | endif 177 | 178 | return self.cache.get('installed_json') 179 | endfunction 180 | 181 | "" 182 | " Query {key} from project's composer.json with [default] value. 183 | function! s:project_query(key, ...) dict abort 184 | let default = get(a:000, 0, '') 185 | return s:get_nested(self.json(), a:key, default) 186 | endfunction 187 | 188 | "" 189 | " Determine if {package} is installed with optional [version] constraint. 190 | " 191 | " composer#project().is_installed('my/package', '>=', '1.2.0') 192 | " 193 | function! s:project_is_installed(package, ...) dict abort 194 | let comparator = get(a:000, 0, '') 195 | let ver = get(a:000, 1, '') 196 | let packages = self.packages_installed() 197 | 198 | call filter(packages, 'v:val.name == a:package') 199 | 200 | if len(packages) < 1 201 | return v:false 202 | endif 203 | 204 | if len(ver) < 1 205 | return v:true 206 | endif 207 | 208 | return composer#semver#compare(packages[0].version_normalized, comparator, ver) 209 | endfunction 210 | 211 | "" 212 | " Get Dict of packages required in composer.json, where the keys represent 213 | " package names and the values represent the version constraints. 214 | function! s:project_packages_required() dict abort 215 | return get(self.json(), 'require', {}) 216 | endfunction 217 | 218 | "" 219 | " Get Dict of packages installed in current project from installed.json. 220 | function! s:project_packages_installed() dict abort 221 | " Older format lists packages at the root, newer format nests under the 222 | " 'packages' key. 223 | return deepcopy(get(self.installed_json(), 'packages', self.installed_json())) 224 | endfunction 225 | 226 | "" 227 | " Get Dict of scripts in composer.json, where the keys represent 228 | " event/command names and the values represent commands. 229 | function! s:project_scripts() dict abort 230 | return get(self.json(), 'scripts', {}) 231 | endfunction 232 | 233 | "" 234 | " Get Composer executable. 235 | function! s:project_makeprg() dict abort 236 | if self.has_file('composer.phar') 237 | return 'php composer.phar' 238 | elseif executable('composer.bat') 239 | return 'composer.bat' 240 | else 241 | return 'composer' 242 | endif 243 | endfunction 244 | 245 | "" 246 | " Get output from Composer with {args} in project's root directory. 247 | function! s:project_exec(args) dict abort 248 | try 249 | let cwd = self.cd(self.path()) 250 | let result = system(join([self.makeprg()] + a:args)) 251 | finally 252 | call self.cd(cwd) 253 | endtry 254 | 255 | return result 256 | endfunction 257 | 258 | "" 259 | " Get Dict of subcommands, optionally belonging to [namespace]. 260 | function! s:project_commands(...) dict abort 261 | let namespace = get(a:000, 0, '') 262 | let cache = namespace ==# '' ? 'commands' : 'commands_' . namespace 263 | 264 | if self.cache.needs(cache) 265 | let lines = split(self.exec(['list', namespace, '--raw']), "\n") 266 | 267 | if v:shell_error != 0 268 | return [] 269 | endif 270 | 271 | call map(lines, "matchstr(v:val, '^.\\{-}\\ze\\s')") 272 | call filter(lines, 'v:val != ""') 273 | 274 | call self.cache.set(cache, lines) 275 | endif 276 | 277 | return self.cache.get(cache) 278 | endfunction 279 | 280 | "" 281 | " Search package names in available repositories. 282 | function! s:project_search(keyword) dict abort 283 | let cache = 'search_' . a:keyword 284 | 285 | if self.cache.needs(cache) 286 | let lines = split(self.exec(['search', '--only-name', a:keyword]), "\n") 287 | 288 | if v:shell_error != 0 289 | return [] 290 | endif 291 | 292 | call map(lines, "matchstr(v:val, '^.\\{-}\\ze\\s')") 293 | call filter(lines, 'v:val != ""') 294 | 295 | call self.cache.set(cache, lines) 296 | endif 297 | 298 | return self.cache.get(cache) 299 | endfunction 300 | 301 | call s:add_methods('project', ['json', 'lock', 'installed_json', 'query', 'is_installed', 'scripts', 'makeprg', 'exec', 'commands', 'packages_required', 'packages_installed', 'search']) 302 | 303 | let s:cache_prototype = {'cache': {}} 304 | 305 | function! s:cache_clear(...) dict abort 306 | if a:0 == 0 307 | let self.cache = {} 308 | elseif has_key(self, 'cache') && has_key(self.cache, a:1) 309 | unlet! self.cache[a:1] 310 | endif 311 | endfunction 312 | 313 | function! composer#cache_clear(...) abort 314 | if exists('b:composer_root') 315 | return call(composer#project().cache.clear, a:000, composer#project().cache) 316 | endif 317 | endfunction 318 | 319 | function! s:cache_get(...) dict abort 320 | if a:0 == 0 321 | return self.cache 322 | else 323 | return self.cache[a:1] 324 | endif 325 | endfunction 326 | 327 | function! s:cache_set(key, value) dict abort 328 | let self.cache[a:key] = a:value 329 | endfunction 330 | 331 | function! s:cache_has(key) dict abort 332 | return has_key(self.cache, a:key) 333 | endfunction 334 | 335 | function! s:cache_needs(key) dict abort 336 | return !has_key(self.cache, a:key) 337 | endfunction 338 | 339 | call s:add_methods('cache', ['clear', 'get', 'set', 'has', 'needs']) 340 | 341 | let s:project_prototype.cache = s:cache_prototype 342 | 343 | augroup composer_cache 344 | autocmd! 345 | autocmd BufWritePost composer.json call composer#cache_clear('json') 346 | autocmd User ComposerCmdPost call composer#cache_clear('json') 347 | autocmd User ComposerCmdPost call composer#cache_clear('lock') 348 | autocmd User ComposerCmdPost call composer#cache_clear('json_installed') 349 | augroup END 350 | 351 | "" 352 | " @public 353 | " Query {key} from composer.json for current project. 354 | function! composer#query(key) abort 355 | return s:project().query(a:key) 356 | endfunction 357 | 358 | "" 359 | " @private 360 | " Set up Composer buffers. 361 | function! composer#buffer_setup() abort 362 | "" 363 | " @command Composer[!] [arguments] 364 | " Invoke Composer with [arguments] (with intelligent completion, including 365 | " completion for package names on packagist.org). 366 | command! -buffer -bang -bar -nargs=? -complete=customlist,composer#commandline#complete 367 | \ Composer execute composer#commandline#exec(, ) 368 | 369 | if &filetype =~# 'php' 370 | "" 371 | " Find definition of class, interface, or trait under the cursor using 372 | " Composer's autoload mechanism. 373 | nnoremap (composer-find) :execute composer#autoload#find() 374 | 375 | "" 376 | " Insert a use statement for the class/interface/trait under the cursor. 377 | nnoremap (composer-use) :execute composer#namespace#use(0) 378 | endif 379 | 380 | silent doautocmd User Composer 381 | endfunction 382 | 383 | "" 384 | " @private 385 | " Hack for testing script-local functions. 386 | function! composer#sid() 387 | nnoremap 388 | return maparg('', 'n') 389 | endfunction 390 | 391 | " vim: fdm=marker:sw=2:sts=2:et 392 | -------------------------------------------------------------------------------- /autoload/composer/autoload.vim: -------------------------------------------------------------------------------- 1 | " autoload/composer/autoload.vim - Find source code via Composer autoloader 2 | " Maintainer: Noah Frederick 3 | 4 | "" 5 | " Throw error with {msg} and replacements. 6 | function! s:throw(...) abort 7 | let msg = a:0 > 1 ? call('printf', a:000) : a:1 8 | let v:errmsg = 'composer: ' . msg 9 | throw v:errmsg 10 | endfunction 11 | 12 | "" 13 | " @private 14 | " Edit source file for class/trait/interface at cursor or specified in 15 | " [class]. The name is automatically expanded into a fully-qualified name. 16 | function! composer#autoload#find(...) abort 17 | let class = get(a:000, 0, composer#namespace#class_at_cursor()) 18 | 19 | if empty(class) 20 | return '' 21 | elseif class[0] ==# '\' 22 | let fqn = class 23 | else 24 | let fqn = composer#namespace#using(class, 1) 25 | 26 | if empty(fqn) 27 | let fqn = composer#namespace#expand(class) 28 | endif 29 | endif 30 | 31 | try 32 | return 'edit ' . s:find_file(fqn) 33 | catch /^composer:/ 34 | endtry 35 | 36 | return '' 37 | endfunction 38 | 39 | "" 40 | " Find source file for the class/trait/interface {fqn} using Composer's 41 | " autoloader. 42 | function! s:find_file(fqn) abort 43 | let project = composer#project() 44 | let autoload = project.vendor_dir('autoload.php') 45 | 46 | if !project.has_file(autoload) 47 | call s:throw('autoload.php not found. Run composer install.') 48 | endif 49 | 50 | let fqn = substitute(a:fqn, '^\', '', '') 51 | let s = '$c = require("' . autoload . '"); echo $c->findFile($argv[1]);' 52 | let path = system('php -r ' . shellescape(s) . ' ' . shellescape(fqn)) 53 | 54 | if v:shell_error != 0 55 | call s:throw('Command exited with code %d', v:shell_error) 56 | endif 57 | 58 | if !project.has_file(path) 59 | call s:throw('Cannot find file for %s', a:fqn) 60 | endif 61 | 62 | return simplify(path) 63 | endfunction 64 | 65 | "" 66 | " @private 67 | " Hack for testing script-local functions. 68 | function! composer#autoload#sid() 69 | nnoremap 70 | return maparg('', 'n') 71 | endfunction 72 | 73 | " vim: fdm=marker:sw=2:sts=2:et 74 | -------------------------------------------------------------------------------- /autoload/composer/commandline.vim: -------------------------------------------------------------------------------- 1 | " autoload/composer/commandline.vim - Commandline support 2 | " Maintainer: Noah Frederick 3 | 4 | "" 5 | " Implement the one-argument version of uniq() for older Vims. 6 | function! s:uniq(list) abort 7 | if exists('*uniq') 8 | return uniq(a:list) 9 | endif 10 | 11 | let i = 0 12 | let last = '' 13 | 14 | while i < len(a:list) 15 | let str = string(a:list[i]) 16 | if str ==# last && i > 0 17 | call remove(a:list, i) 18 | else 19 | let last = str 20 | let i += 1 21 | endif 22 | endwhile 23 | 24 | return a:list 25 | endfunction 26 | 27 | "" 28 | " @private 29 | " The :Composer command. Execute !composer in the project root. 30 | function! composer#commandline#exec(...) abort 31 | let args = copy(a:000) 32 | let bang = remove(args, 0) 33 | let project = composer#project() 34 | 35 | let g:composer_cmd_args = args 36 | silent doautocmd User ComposerCmdPre 37 | 38 | if exists(':terminal') 39 | tabedit % 40 | execute 'lcd' fnameescape(project.path()) 41 | execute 'terminal' project.makeprg() join(args) 42 | else 43 | let cwd = project.cd() 44 | execute '!' . project.makeprg() join(args) 45 | call project.cd(cwd) 46 | endif 47 | 48 | silent doautocmd User ComposerCmdPost 49 | unlet! g:composer_cmd_args 50 | 51 | return '' 52 | endfunction 53 | 54 | "" 55 | " @private 56 | " Completion for the :Composer command, including completion of: 57 | " - Global flags 58 | " - Built-in subcommands 59 | " - Subcommand-specific flags 60 | " - Package names 61 | " - Scripts defined in composer.json 62 | function! composer#commandline#complete(A, L, P) abort 63 | let commands = copy(composer#project().commands()) 64 | 65 | call remove(commands, index(commands, 'global')) 66 | call remove(commands, index(commands, 'help')) 67 | let subcommand = matchstr(a:L, '\<\(' . join(commands, '\|') . '\)\>') 68 | let global = matchstr(a:L, '\') 69 | let help = matchstr(a:L, '\') 70 | 71 | let candidates = s:flags['_global'] 72 | 73 | if empty(subcommand) 74 | let candidates = candidates + commands 75 | 76 | if empty(global) 77 | let candidates = candidates + ['global', 'help'] 78 | else 79 | let candidates = candidates + ['help'] 80 | endif 81 | elseif has_key(s:flags, subcommand) 82 | let candidates = candidates + s:flags[subcommand] 83 | endif 84 | 85 | if empty(help) && index(['depends', 'remove', 'update', 'suggests'], subcommand) >= 0 86 | try 87 | let candidates = candidates + keys(composer#project().packages_required()) 88 | catch 89 | " Fail silently when composer.json cannot be parsed because of missing 90 | " dependency or invalid/empty file. 91 | endtry 92 | endif 93 | 94 | if empty(help) && index(['run-script', ''], subcommand) >= 0 95 | try 96 | let candidates = candidates + keys(composer#project().scripts()) 97 | catch 98 | " Fail silently when composer.json cannot be parsed because of missing 99 | " dependency or invalid/empty file. 100 | endtry 101 | endif 102 | 103 | if empty(help) && index(['browse', 'home', 'require', 'show'], subcommand) >= 0 && !empty(a:A) 104 | let candidates = candidates + composer#project().search(a:A) 105 | endif 106 | 107 | return s:filter_completions(candidates, a:A) 108 | endfunction 109 | 110 | "" 111 | " Sort and filter completion {candidates} based on the current argument {A}. 112 | " Adapted from bundler.vim. 113 | function! s:filter_completions(candidates, A) abort 114 | let candidates = copy(a:candidates) 115 | if len(candidates) == 0 116 | return [] 117 | endif 118 | call sort(candidates) 119 | call s:uniq(candidates) 120 | 121 | let commands = filter(copy(candidates), "v:val[0] !=# '-'") 122 | let flags = filter(copy(candidates), "v:val[0] ==# '-'") 123 | 124 | let candidates = commands + flags 125 | 126 | let filtered = filter(copy(candidates), 'v:val[0:strlen(a:A)-1] ==# a:A') 127 | if !empty(filtered) | return filtered | endif 128 | 129 | let regex = substitute(a:A, '[^/:]', '[&].*', 'g') 130 | let filtered = filter(copy(candidates), 'v:val =~# "^".regex') 131 | if !empty(filtered) | return filtered | endif 132 | 133 | let filtered = filter(copy(candidates), '"/".v:val =~# "[/:]".regex') 134 | if !empty(filtered) | return filtered | endif 135 | 136 | let regex = substitute(a:A, '.', '[&].*', 'g') 137 | let filtered = filter(copy(candidates),'"/".v:val =~# regex') 138 | return filtered 139 | endfunction 140 | 141 | " Unlike subcommands, composer does not list switches/flags in a friendly 142 | " format, so we hard-code them. 143 | let s:flags = { 144 | \ '_global': [ 145 | \ '--xml', 146 | \ '--format', 147 | \ '--raw', 148 | \ '--help', 149 | \ '-h', 150 | \ '--quiet', 151 | \ '-q', 152 | \ '--verbose', 153 | \ '-v', 154 | \ '-vv', 155 | \ '-vvv', 156 | \ '--version', 157 | \ '-V', 158 | \ '--ansi', 159 | \ '--no-ansi', 160 | \ '--no-interaction', 161 | \ '-n', 162 | \ '--profile', 163 | \ '--working-dir', 164 | \ '-d', 165 | \ ], 166 | \ 'install': [ 167 | \ '--prefer-source', 168 | \ '--prefer-dist', 169 | \ '--ignore-platform-reqs', 170 | \ '--dry-run', 171 | \ '--dev', 172 | \ '--no-dev', 173 | \ '--no-autoloader', 174 | \ '--no-scripts', 175 | \ '--no-plugins', 176 | \ '--no-progress', 177 | \ '--optimize-autoloader', 178 | \ '-o', 179 | \ ], 180 | \ 'update': [ 181 | \ '--prefer-source', 182 | \ '--prefer-dist', 183 | \ '--ignore-platform-reqs', 184 | \ '--dry-run', 185 | \ '--dev', 186 | \ '--no-dev', 187 | \ '--no-autoloader', 188 | \ '--no-scripts', 189 | \ '--no-plugins', 190 | \ '--no-progress', 191 | \ '--optimize-autoloader', 192 | \ '-o', 193 | \ '--lock', 194 | \ '--with-dependencies', 195 | \ '--prefer-stable', 196 | \ '--prefer-lowest', 197 | \ ], 198 | \ 'require': [ 199 | \ '--prefer-source', 200 | \ '--prefer-dist', 201 | \ '--ignore-platform-reqs', 202 | \ '--dev', 203 | \ '--no-update', 204 | \ '--no-progress', 205 | \ '--update-no-dev', 206 | \ '--update-with-dependencies', 207 | \ '--sort-packages', 208 | \ ], 209 | \ 'remove': [ 210 | \ '--ignore-platform-reqs', 211 | \ '--dev', 212 | \ '--no-update', 213 | \ '--no-progress', 214 | \ '--update-no-dev', 215 | \ '--update-with-dependencies', 216 | \ ], 217 | \ 'search': [ 218 | \ '--only-name', 219 | \ '-N', 220 | \ ], 221 | \ 'show': [ 222 | \ '--installed', 223 | \ '-i', 224 | \ '--platform', 225 | \ '-p', 226 | \ '--self', 227 | \ '-s', 228 | \ ], 229 | \ 'browse': [ 230 | \ '--homepage', 231 | \ '-H', 232 | \ ], 233 | \ 'home': [ 234 | \ '--homepage', 235 | \ '-H', 236 | \ ], 237 | \ 'suggests': [ 238 | \ '--no-dev', 239 | \ '--verbose', 240 | \ '-v', 241 | \ ], 242 | \ 'depends': [ 243 | \ '--link-type', 244 | \ ], 245 | \ 'validate': [ 246 | \ '--no-check-all', 247 | \ '--no-check-lock', 248 | \ '--no-check-publish', 249 | \ ], 250 | \ 'status': [ 251 | \ ], 252 | \ 'self-update': [ 253 | \ '--rollback', 254 | \ '-r', 255 | \ '--clean-backups', 256 | \ ], 257 | \ 'config': [ 258 | \ '--global', 259 | \ '-g', 260 | \ '--editor', 261 | \ '-e', 262 | \ '--unset', 263 | \ '--list', 264 | \ '-l', 265 | \ '--file', 266 | \ '-f', 267 | \ '--absolute', 268 | \ ], 269 | \ 'create-project': [ 270 | \ '--repository-url', 271 | \ '--stability', 272 | \ '-s', 273 | \ '--prefer-source', 274 | \ '--prefer-dist', 275 | \ '--dev', 276 | \ '--no-install', 277 | \ '--no-plugins', 278 | \ '--no-scripts', 279 | \ '--no-progress', 280 | \ '--keep-vcs', 281 | \ '--ignore-platform-reqs', 282 | \ ], 283 | \ 'dump-autoload': [ 284 | \ '--optimize', 285 | \ '-o', 286 | \ '--no-dev', 287 | \ ], 288 | \ 'clear-cache': [ 289 | \ ], 290 | \ 'licenses': [ 291 | \ '--no-dev', 292 | \ '--format', 293 | \ ], 294 | \ 'run-script': [ 295 | \ '--no-dev', 296 | \ '--list', 297 | \ ], 298 | \ 'diagnose': [ 299 | \ ], 300 | \ 'archive': [ 301 | \ '--format', 302 | \ '-f', 303 | \ '--dir', 304 | \ ], 305 | \ } 306 | 307 | "" 308 | " @private 309 | " Hack for testing script-local functions. 310 | function! composer#commandline#sid() 311 | nnoremap 312 | return maparg('', 'n') 313 | endfunction 314 | 315 | " vim: fdm=marker:sw=2:sts=2:et 316 | -------------------------------------------------------------------------------- /autoload/composer/namespace.vim: -------------------------------------------------------------------------------- 1 | " autoload/composer/namespace.vim - Namespacing and use statements 2 | " Maintainer: Noah Frederick 3 | 4 | "" 5 | " @private 6 | " Insert use statement for [class], optionally with [alias]. If {sort} is 7 | " non-empty, also sort all use statements in the buffer. 8 | function! composer#namespace#use(sort, ...) abort 9 | let class = get(a:000, 0, composer#namespace#class_at_cursor()) 10 | let alias = get(a:000, 1, '') 11 | let sort = !empty(a:sort) 12 | 13 | if !empty(composer#namespace#using(empty(alias) ? class : alias)) 14 | echohl WarningMsg 15 | echomsg 'Use statement for ' . class . ' already exists' 16 | echohl None 17 | return 18 | endif 19 | 20 | let fqn = composer#namespace#expand(class) 21 | let line = 'use ' . fqn[1:-1] 22 | 23 | if !empty(alias) 24 | let line .= ' as ' . alias 25 | endif 26 | 27 | let line .= ';' 28 | 29 | if search('^use\_s\_[[:alnum:][:blank:]\\,_]\+;', 'wbe') > 0 30 | put=line 31 | elseif search('^\s*namespace\_s\_[[:alnum:]\\_]\+;', 'wbe') > 0 32 | put='' 33 | put=line 34 | elseif search(' 0 35 | put='' 36 | put=line 37 | else 38 | 0put=line 39 | endif 40 | 41 | if sort 42 | call composer#namespace#sort_uses() 43 | endif 44 | 45 | return '' 46 | endfunction 47 | 48 | "" 49 | " @private 50 | " Sort use statements in buffer alphabetically. 51 | function! composer#namespace#sort_uses() abort 52 | let save = @a 53 | let @a = '' 54 | 55 | normal! m` 56 | 57 | " Collapse multiline use statements into single lines 58 | while search('^use\_s\_[[:alnum:][:blank:]\\,_]\+,$') > 0 59 | global/^use\_s\_[[:alnum:][:blank:]\\,_]\+,$/join 60 | endwhile 61 | 62 | " Gather all use statements 63 | global/^use\_s\_[[:alnum:][:blank:]\\,_]\+;/delete A 64 | 65 | if search('^\s*namespace\_s\_[[:alnum:]\\_]\+;', 'wbe') > 0 66 | put a 67 | elseif search(' 0 68 | put a 69 | else 70 | 0put a 71 | endif 72 | 73 | '[,']sort 74 | 75 | " Clean up blank line after pasted use block 76 | ']+1delete _ 77 | 78 | normal! `` 79 | 80 | let @a = save 81 | endfunction 82 | 83 | "" 84 | " @private 85 | " Find use statement matching {class}. If [allow_aliased] is non-zero, allow 86 | " matching a class name before an 'as'. Adapted from 87 | " https://github.com/arnaud-lb/vim-php-namespace/blob/master/plugin/phpns.vim 88 | function! composer#namespace#using(class, ...) abort 89 | let class = escape(substitute(a:class, '^\\', '', ''), '\') 90 | let allow_aliased = get(a:000, 0, 0) 91 | 92 | " Matches: use Foo\Bar as {class}; 93 | let pattern = '\%(^\|\r\|\n\)\s*use\_s\+\_[^;]\{-}\_s*\([^;,]*\)\_s\+as\_s\+' . class . '\_s*[;,]' 94 | let fqn = s:capture(pattern, 1) 95 | if fqn isnot 0 96 | return fqn 97 | endif 98 | 99 | " Matches: use Foo\{class}; 100 | let pattern = '\%(^\|\r\|\n\)\s*use\_s\+\_[^;]\{-}\_s*\([^;,]*' . class . '\)\_s*[;,]' 101 | let fqn = s:capture(pattern, 1) 102 | if fqn isnot 0 103 | return fqn 104 | endif 105 | 106 | if allow_aliased 107 | " Matches: use {class} as Bar; 108 | let pattern = '\%(^\|\r\|\n\)\s*use\_s\+\_[^;]\{-}\_s*\([^;,]*' . class . '\)' 109 | let fqn = s:capture(pattern, 1) 110 | if fqn isnot 0 111 | return fqn 112 | endif 113 | endif 114 | 115 | return '' 116 | endfunction 117 | 118 | "" 119 | " @private 120 | " Expand {class} to fully-qualified name in the context of the current file's 121 | " namespace. 122 | function! composer#namespace#expand(class) abort 123 | if a:class[0] ==# '\' 124 | return a:class 125 | endif 126 | 127 | let pattern = '\%( 197 | return maparg('', 'n') 198 | endfunction 199 | 200 | " vim: fdm=marker:sw=2:sts=2:et 201 | -------------------------------------------------------------------------------- /autoload/composer/semver.vim: -------------------------------------------------------------------------------- 1 | " autoload/composer/semver.vim - Semver parsing and comparison 2 | " Maintainer: Noah Frederick 3 | 4 | "" 5 | " @private 6 | " Parse semver version string into object. 7 | function! composer#semver#parse(version_string) abort 8 | let semver = {} 9 | let parts = matchlist(a:version_string, '\v^(\d+)%(.(\d+)%(.(\d+))?)?') 10 | 11 | let semver.major = str2nr(get(parts, 1, '')) 12 | let semver.minor = str2nr(get(parts, 2, '')) 13 | let semver.patch = str2nr(get(parts, 3, '')) 14 | 15 | return semver 16 | endfunction 17 | 18 | "" 19 | " @private 20 | " Compare semver version strings. 21 | function! composer#semver#compare(a, comparator, b) abort 22 | if !has_key(s:comparators, a:comparator) 23 | echoerr 'Composer semver: ' . a:comparator . ' is not a valid comparator' 24 | return v:false 25 | endif 26 | 27 | let a = composer#semver#parse(a:a) 28 | let b = composer#semver#parse(a:b) 29 | 30 | if function(s:comparators[a:comparator], [a, b])() is v:false 31 | return v:false 32 | endif 33 | 34 | return v:true 35 | endfunction 36 | 37 | let s:comparators = { 38 | \ '==': 's:is_equal', 39 | \ '!=': 's:is_not_equal', 40 | \ '>': 's:is_greater_than', 41 | \ '>=': 's:is_greater_than_or_equal', 42 | \ '<': 's:is_less_than', 43 | \ '<=': 's:is_less_than_or_equal', 44 | \ } 45 | 46 | function! s:is_equal(a, b) abort 47 | if a:a.major != a:b.major 48 | return v:false 49 | endif 50 | 51 | if a:a.minor != a:b.minor 52 | return v:false 53 | endif 54 | 55 | if a:a.patch != a:b.patch 56 | return v:false 57 | endif 58 | 59 | return v:true 60 | endfunction 61 | 62 | function! s:is_not_equal(a, b) abort 63 | return s:is_equal(a:a, a:b) ? v:false : v:true 64 | endfunction 65 | 66 | function! s:is_greater_than(a, b) abort 67 | if s:is_equal(a:a, a:b) 68 | return v:false 69 | endif 70 | 71 | if a:a.major > a:b.major 72 | return v:true 73 | elseif a:a.major == a:b.major 74 | if a:a.minor > a:b.minor 75 | return v:true 76 | elseif a:a.minor == a:b.minor 77 | return a:a.patch > a:b.patch 78 | endif 79 | endif 80 | 81 | return v:false 82 | endfunction 83 | 84 | function! s:is_less_than(a, b) abort 85 | return ((s:is_greater_than(a:a, a:b) || s:is_equal(a:a, a:b))) ? v:false : v:true 86 | endfunction 87 | 88 | function! s:is_greater_than_or_equal(a, b) 89 | return ((s:is_greater_than(a:a, a:b) || s:is_equal(a:a, a:b))) ? v:true : v:false 90 | endfunction 91 | 92 | function! s:is_less_than_or_equal(a, b) 93 | return ((s:is_less_than(a:a, a:b) || s:is_equal(a:a, a:b))) ? v:true : v:false 94 | endfunction 95 | 96 | " vim: fdm=marker:sw=2:sts=2:et 97 | -------------------------------------------------------------------------------- /doc/composer.txt: -------------------------------------------------------------------------------- 1 | *composer.txt* Vim support for Composer PHP projects 2 | Noah Frederick *Composer.vim* *composer* 3 | 4 | ============================================================================== 5 | CONTENTS *composer-contents* 6 | 1. Introduction.............................................|composer-intro| 7 | 2. Commands..............................................|composer-commands| 8 | 3. Mappings..................................................|composer-maps| 9 | 4. Autocommands......................................|composer-autocommands| 10 | 5. Functions............................................|composer-functions| 11 | 6. About....................................................|composer-about| 12 | 13 | ============================================================================== 14 | INTRODUCTION *composer-intro* 15 | 16 | Composer.vim provides conveniences for working with Composer PHP projects. 17 | Some features include: 18 | 19 | * |:Composer| command wrapper around composer with smart completion 20 | * Navigate to source files using Composer's autoloader 21 | * Insert use statement for the class/interface/trait under cursor 22 | * Projectionist support (e.g., :Ecomposer to edit your composer.json, :A to 23 | jump to composer.lock and back) 24 | * Dispatch support (|:Dispatch| runs composer dump-autoload) 25 | 26 | This plug-in is only available if 'compatible' is not set. 27 | 28 | ============================================================================== 29 | COMMANDS *composer-commands* 30 | 31 | :Composer[!] [arguments] *:Composer* 32 | Invoke Composer with [arguments] (with intelligent completion, including 33 | completion for package names on packagist.org). 34 | 35 | ============================================================================== 36 | MAPPINGS *composer-maps* 37 | 38 | (composer-find) 39 | 40 | Find the definition of class, interface, or trait under the cursor using 41 | Composer's autoload mechanism. It is namespace-aware and even resolves aliases 42 | in 'use' statements. For example, it does what you want with the cursor on 43 | 'Env' in the last line: 44 | > 45 | (composer-use) 53 | 54 | Insert a use statement for the class, interface, or trait under the cursor. 55 | 56 | ============================================================================== 57 | AUTOCOMMANDS *composer-autocommands* 58 | 59 | If you want to set your own Vim settings for buffers belonging to your 60 | Composer project, you may do so from your vimrc using an autocommand: 61 | > 62 | autocmd User Composer nmap gf (composer-find) 63 | < 64 | 65 | ============================================================================== 66 | FUNCTIONS *composer-functions* 67 | 68 | composer#query({key}) *composer#query()* 69 | Query {key} from composer.json for current project. 70 | 71 | ============================================================================== 72 | ABOUT *composer-about* 73 | 74 | Composer.vim is distributed under the same terms as Vim itself (see |license|) 75 | 76 | You can find the latest version of this plug-in on GitHub: 77 | https://github.com/noahfrederick/vim-composer 78 | 79 | Please report issues on GitHub as well: 80 | https://github.com/noahfrederick/vim-composer/issues 81 | 82 | 83 | vim:tw=78:ts=8:ft=help:norl: 84 | -------------------------------------------------------------------------------- /plugin/composer.vim: -------------------------------------------------------------------------------- 1 | " plugin/composer.vim - Composer for Vim 2 | " Maintainer: Noah Frederick (https://noahfrederick.com) 3 | 4 | "" 5 | " @section Introduction, intro 6 | " @stylized Composer.vim 7 | " @order intro commands maps autocommands functions about 8 | " @plugin(stylized) provides conveniences for working with Composer PHP 9 | " projects. Some features include: 10 | " 11 | " * @command(:Composer) command wrapper around composer with smart completion 12 | " * Navigate to source files using Composer's autoloader 13 | " * Insert use statement for the class/interface/trait under cursor 14 | " * Projectionist support (e.g., :Ecomposer to edit your composer.json, :A to 15 | " jump to composer.lock and back) 16 | " * Dispatch support (|:Dispatch| runs composer dump-autoload) 17 | " 18 | " This plug-in is only available if 'compatible' is not set. 19 | 20 | "" 21 | " @section About, about 22 | " @plugin(stylized) is distributed under the same terms as Vim itself (see 23 | " |license|) 24 | " 25 | " You can find the latest version of this plug-in on GitHub: 26 | " https://github.com/noahfrederick/vim-@plugin(name) 27 | " 28 | " Please report issues on GitHub as well: 29 | " https://github.com/noahfrederick/vim-@plugin(name)/issues 30 | 31 | "" 32 | " @section Mappings, maps 33 | " (composer-find) 34 | " 35 | " Find the definition of class, interface, or trait under the cursor using 36 | " Composer's autoload mechanism. It is namespace-aware and even resolves 37 | " aliases in 'use' statements. For example, it does what you want with the 38 | " cursor on 'Env' in the last line: > 39 | " (composer-use) 47 | " 48 | " Insert a use statement for the class, interface, or trait under the cursor. 49 | 50 | "" 51 | " @section Autocommands, autocommands 52 | " If you want to set your own Vim settings for buffers belonging to your 53 | " Composer project, you may do so from your vimrc using an autocommand: > 54 | " autocmd User Composer nmap gf (composer-find) 55 | " < 56 | 57 | if (exists('g:loaded_composer') && g:loaded_composer) || &cp 58 | finish 59 | endif 60 | let g:loaded_composer = 1 61 | 62 | " Detection {{{ 63 | 64 | "" 65 | " Determine whether the current or supplied [path] belongs to a Composer 66 | " project, and set b:composer_root to the path of the project root. 67 | function! s:composer_detect(...) abort 68 | if exists('b:composer_root') 69 | return 1 70 | endif 71 | 72 | let fn = fnamemodify(get(a:000, 0, expand('%')), ':p') 73 | 74 | if !isdirectory(fn) 75 | let fn = fnamemodify(fn, ':h') 76 | endif 77 | 78 | let candidates = findfile('composer.json', escape(fn, ', ') . ';', -1) 79 | 80 | for json in candidates 81 | let root = fnamemodify(json, ':p:h') 82 | 83 | if filereadable(root.'/vendor/autoload.php') 84 | let b:composer_root = root 85 | return 1 86 | endif 87 | endfor 88 | 89 | if !empty(candidates) 90 | let b:composer_root = fnamemodify(candidates[0], ':p:h') 91 | return 1 92 | endif 93 | endfunction 94 | 95 | " }}} 96 | " Initialization {{{ 97 | 98 | augroup composer_detect 99 | autocmd! 100 | " Project detection 101 | autocmd BufNewFile,BufReadPost * 102 | \ if s:composer_detect(expand(":p")) && empty(&filetype) | 103 | \ call composer#buffer_setup() | 104 | \ endif 105 | autocmd VimEnter * 106 | \ if empty(expand("")) && s:composer_detect(getcwd()) | 107 | \ call composer#buffer_setup() | 108 | \ endif 109 | autocmd FileType * if s:composer_detect() | call composer#buffer_setup() | endif 110 | autocmd BufNewFile,BufRead composer.lock setf json 111 | augroup END 112 | 113 | " }}} 114 | " Projections {{{ 115 | 116 | " Ensure that projectionist gets loaded first 117 | if !exists('g:loaded_projectionist') 118 | runtime! plugin/projectionist.vim 119 | endif 120 | 121 | function! s:projectionist_detect() 122 | if s:composer_detect(get(g:, 'projectionist_file', '')) 123 | let dispatch = join([composer#project().makeprg(), 'dump-autoload']) 124 | 125 | call projectionist#append(b:composer_root, { 126 | \ "*": { 127 | \ "dispatch": dispatch, 128 | \ }, 129 | \ "composer.json": { 130 | \ "type": "composer", 131 | \ "alternate": "composer.lock", 132 | \ }, 133 | \ "composer.lock": { 134 | \ "type": "composerlock", 135 | \ "alternate": "composer.json", 136 | \ }}) 137 | endif 138 | endfunction 139 | 140 | augroup composer_projections 141 | autocmd! 142 | autocmd User ProjectionistDetect call s:projectionist_detect() 143 | augroup END 144 | 145 | " }}} 146 | 147 | " vim: fdm=marker:sw=2:sts=2:et 148 | -------------------------------------------------------------------------------- /t/activation.vim: -------------------------------------------------------------------------------- 1 | " t/activation.vim - Activation tests 2 | " Maintainer: Noah Frederick 3 | 4 | let g:fixtures = fnamemodify('t/fixtures/', ':p') 5 | 6 | runtime plugin/composer.vim 7 | 8 | augroup composer_test 9 | autocmd! 10 | autocmd User Composer let b:did_autocommand = 1 11 | augroup END 12 | 13 | describe 's:composer_detect()' 14 | after 15 | bwipeout! 16 | end 17 | 18 | context 'in a non-composer project' 19 | it 'does not set b:composer_root' 20 | execute 'edit' g:fixtures . 'project-other/index.php' 21 | Expect exists('b:composer_root') to_be_false 22 | end 23 | end 24 | 25 | context 'in the root of a composer project' 26 | it 'sets b:composer_root' 27 | execute 'edit' g:fixtures . 'project-composer/index.php' 28 | Expect exists('b:composer_root') to_be_true 29 | Expect b:composer_root == g:fixtures . 'project-composer' 30 | end 31 | end 32 | 33 | context 'in a nested directory of a composer project' 34 | it 'sets b:composer_root' 35 | execute 'edit' g:fixtures . 'project-composer/src/index.php' 36 | Expect exists('b:composer_root') to_be_true 37 | Expect b:composer_root == g:fixtures . 'project-composer' 38 | end 39 | end 40 | 41 | context 'in a dependency of a composer project' 42 | it 'sets b:composer_root to the root project' 43 | execute 'edit' g:fixtures . 'project-composer/vendor/foo/bar/index.php' 44 | Expect exists('b:composer_root') to_be_true 45 | Expect b:composer_root == g:fixtures . 'project-composer' 46 | end 47 | end 48 | 49 | context 'in a composer project without a vendor autoload' 50 | it 'sets b:composer_root' 51 | execute 'edit' g:fixtures . 'project-uninstalled/index.php' 52 | Expect exists('b:composer_root') to_be_true 53 | Expect b:composer_root == g:fixtures . 'project-uninstalled' 54 | end 55 | end 56 | end 57 | 58 | describe 'composer#buffer_setup()' 59 | after 60 | bwipeout! 61 | end 62 | 63 | context 'in a non-composer project' 64 | before 65 | execute 'edit' g:fixtures . 'project-other/index.php' 66 | end 67 | 68 | it 'does not define the :Composer command' 69 | Expect exists(':Composer') != 2 70 | end 71 | 72 | it 'does not fire user autocommand' 73 | Expect exists('b:did_autocommand') to_be_false 74 | end 75 | end 76 | 77 | context 'in a composer project' 78 | before 79 | filetype plugin on 80 | execute 'edit' g:fixtures . 'project-composer/index.php' 81 | end 82 | 83 | it 'defines the :Composer command' 84 | Expect exists(':Composer') == 2 85 | end 86 | 87 | it 'fires user autocommand' 88 | Expect exists('b:did_autocommand') to_be_true 89 | end 90 | end 91 | 92 | context 'editing a non-PHP file in a composer project' 93 | before 94 | execute 'edit' g:fixtures . 'project-composer/foo' 95 | end 96 | 97 | it 'fires user autocommand' 98 | Expect exists('b:did_autocommand') to_be_true 99 | end 100 | end 101 | end 102 | 103 | " vim: fdm=marker:sw=2:sts=2:et 104 | -------------------------------------------------------------------------------- /t/completion.vim: -------------------------------------------------------------------------------- 1 | " t/completion.vim - Completion tests 2 | " Maintainer: Noah Frederick 3 | 4 | let s:fixtures = fnamemodify('t/fixtures/', ':p') 5 | let s:composer_commands = ['global', 'install', 'update', 'suggests', 'remove', 'help', 'run-script'] 6 | 7 | call vspec#hint({'sid': 'composer#commandline#sid()'}) 8 | 9 | " Mock s:project().commands() and s:project().json() 10 | let b:composer_root = s:fixtures . 'project-composer/' 11 | let s:project = composer#project(b:composer_root) 12 | call s:project.cache.set('commands', s:composer_commands) 13 | call s:project.cache.set('json', { 14 | \ 'require': {'some/package': '1.0.0'}, 15 | \ 'scripts': { 16 | \ 'pre-install-cmd': 'foo', 17 | \ 'custom-command': 'bar', 18 | \ }, 19 | \ }) 20 | 21 | describe 's:uniq()' 22 | it 'returns a list' 23 | let result = vspec#call('s:uniq', []) 24 | Expect type(result) == type([]) 25 | end 26 | 27 | it 'removes adjacent duplicates' 28 | let l = ['a', 'b', 'a', 'a', 'c', 'b', 'b', 'b'] 29 | let result = vspec#call('s:uniq', l) 30 | Expect result == ['a', 'b', 'a', 'c', 'b'] 31 | end 32 | end 33 | 34 | describe 's:filter_completions()' 35 | it 'returns a list of completions' 36 | let result = vspec#call('s:filter_completions', [], '') 37 | Expect type(result) == type([]) 38 | end 39 | 40 | it 'sorts the completions alphabetically' 41 | let candidates = ['b', 'a', '-a', '-b'] 42 | let result = vspec#call('s:filter_completions', candidates, '') 43 | Expect index(result, 'a') < index(result, 'b') 44 | Expect index(result, '-a') < index(result, '-b') 45 | end 46 | 47 | it 'sorts non-flags before flags' 48 | let candidates = ['-a', 'b', '--foo', 'a'] 49 | let result = vspec#call('s:filter_completions', candidates, '') 50 | Expect index(result, 'a') < index(result, '-a') 51 | Expect index(result, 'b') < index(result, '-a') 52 | Expect index(result, '--foo') < index(result, '-a') 53 | end 54 | 55 | it 'removes duplicates' 56 | let candidates = ['a', 'b', 'a', 'a'] 57 | let result = vspec#call('s:filter_completions', candidates, '') 58 | Expect result == ['a', 'b'] 59 | end 60 | 61 | it 'filters completions based on ArgLead' 62 | let candidates = ['global', 'help', 'install', 'update'] 63 | let result = vspec#call('s:filter_completions', candidates, 'he') 64 | Expect result == ['help'] 65 | end 66 | 67 | it 'falls back to fuzzy matching' 68 | let candidates = ['global', 'help', 'install', 'update'] 69 | let result = vspec#call('s:filter_completions', candidates, 'hl') 70 | Expect result == ['help'] 71 | end 72 | end 73 | 74 | describe 'composer#commandline#complete()' 75 | it 'returns a list of completions' 76 | Expect type(composer#commandline#complete('', '', 0)) == type([]) 77 | end 78 | 79 | context 'with no preceding arguments' 80 | it 'returns a list containing all commands' 81 | for cmd in s:composer_commands 82 | Expect index(composer#commandline#complete('', '', 0), cmd) >= 0 83 | endfor 84 | end 85 | 86 | it 'returns a list containing custom commands' 87 | Expect index(composer#commandline#complete('', '', 0), 'custom-command') >= 0 88 | end 89 | 90 | it 'returns a list containing global flags' 91 | Expect index(composer#commandline#complete('', '', 0), '--xml') >= 0 92 | end 93 | 94 | it 'returns a list excluding subcommand flags' 95 | Expect index(composer#commandline#complete('', '', 0), '--sort-packages') == -1 96 | end 97 | end 98 | 99 | context 'with global argument' 100 | it 'returns a list containing commands modifyable by global' 101 | let cmds = composer#commandline#complete('', 'global ', 6) 102 | Expect index(cmds, 'global') == -1 103 | Expect index(cmds, 'help') >= 0 104 | Expect index(cmds, 'install') >= 0 105 | Expect index(cmds, 'update') >= 0 106 | end 107 | 108 | it 'returns a list containing global flags' 109 | Expect index(composer#commandline#complete('', 'global ', 6), '--xml') >= 0 110 | end 111 | 112 | it 'filters completions based on ArgLead' 113 | Expect composer#commandline#complete('in', 'global in', 8) == ['install'] 114 | end 115 | end 116 | 117 | context 'with help argument' 118 | it 'returns a list containing commands modifyable by help' 119 | let cmds = composer#commandline#complete('', 'help ', 5) 120 | Expect index(cmds, 'global') >= 0 121 | Expect index(cmds, 'help') >= 0 122 | Expect index(cmds, 'install') >= 0 123 | Expect index(cmds, 'update') >= 0 124 | end 125 | 126 | it 'returns a list containing global flags' 127 | Expect index(composer#commandline#complete('', 'help ', 5), '--xml') >= 0 128 | end 129 | 130 | it 'filters completions based on ArgLead' 131 | Expect composer#commandline#complete('in', 'help in', 7) == ['install'] 132 | end 133 | end 134 | 135 | context 'with run-script argument' 136 | it 'returns a list containing script events' 137 | Expect index(composer#commandline#complete('', 'run-script ', 11), 'pre-install-cmd') >= 0 138 | end 139 | 140 | it 'excludes events with no scripts' 141 | Expect index(composer#commandline#complete('', 'run-script ', 11), 'post-root-package-install') == -1 142 | end 143 | end 144 | 145 | context 'with subcommand argument' 146 | it 'does not return commands' 147 | let cmds = composer#commandline#complete('', 'install ', 8) 148 | Expect index(cmds, 'global') == -1 149 | Expect index(cmds, 'help') == -1 150 | Expect index(cmds, 'install') == -1 151 | Expect index(cmds, 'update') == -1 152 | end 153 | 154 | it 'returns a list containing global flags' 155 | Expect index(composer#commandline#complete('', 'install ', 8), '--xml') >= 0 156 | Expect index(composer#commandline#complete('', 'global install ', 15), '--xml') >= 0 157 | end 158 | 159 | it 'returns a list containing subcommand-specific flags' 160 | Expect index(composer#commandline#complete('', 'install ', 8), '-o') >= 0 161 | Expect index(composer#commandline#complete('', 'global install ', 15), '-o') >= 0 162 | end 163 | end 164 | 165 | context 'with subcommands that take a required package as argument' 166 | it 'returns a list containing required packages' 167 | Expect index(composer#commandline#complete('', 'remove ', 7), 'some/package') >= 0 168 | Expect index(composer#commandline#complete('', 'update ', 7), 'some/package') >= 0 169 | Expect index(composer#commandline#complete('', 'suggests ', 9), 'some/package') >= 0 170 | Expect index(composer#commandline#complete('', 'help ', 5), 'some/package') == -1 171 | Expect index(composer#commandline#complete('', 'help remove ', 12), 'some/package') == -1 172 | end 173 | end 174 | end 175 | 176 | " vim: fdm=marker:sw=2:sts=2:et 177 | -------------------------------------------------------------------------------- /t/fixtures/project-composer/composer.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noahfrederick/vim-composer/7ad79699a52f9974d8cc133d4a6c9bf988bb578c/t/fixtures/project-composer/composer.json -------------------------------------------------------------------------------- /t/fixtures/project-composer/index.php: -------------------------------------------------------------------------------- 1 | 0 146 | end 147 | end 148 | 149 | context 'given an unqualified name' 150 | it 'expands the name' 151 | call composer#namespace#use(0, 'Baz') 152 | Expect line('$') == 8 153 | Expect search('^use Foo\\Baz;$', 'cw') > 0 154 | end 155 | end 156 | 157 | context 'given an alias' 158 | it 'include an as' 159 | call composer#namespace#use(0, '\Baz', 'Fiz') 160 | Expect line('$') == 8 161 | Expect search('^use Baz as Fiz;$', 'cw') > 0 162 | end 163 | end 164 | 165 | context 'when there is already a matching use statement' 166 | it 'does nothing without an alias' 167 | call composer#namespace#use(0, 'Bar') 168 | Expect line('$') == 7 169 | call composer#namespace#use(0, '\Bar') 170 | Expect line('$') == 7 171 | end 172 | 173 | it 'inserts a new use statement with an alias' 174 | call composer#namespace#use(0, '\Bar', 'Fiz') 175 | Expect line('$') == 8 176 | Expect search('^use Bar as Fiz;$', 'cw') > 0 177 | end 178 | end 179 | end 180 | 181 | describe 'placement' 182 | after 183 | bwipeout! 184 | end 185 | 186 | context 'in a buffer with existing use statements' 187 | before 188 | enew 189 | setf php 190 | 0put = '' 59 | Expect composer#semver#compare('1.4.1', '>', '1.0.1') is v:true 60 | Expect composer#semver#compare('1.0.1', '>', '2.0.0') is v:false 61 | Expect composer#semver#compare('1.0.1', '>', '1.0.1') is v:false 62 | Expect composer#semver#compare('1.0.1', '>', '1') is v:false 63 | end 64 | 65 | it 'compares with >=' 66 | Expect composer#semver#compare('1.4.1', '>=', '1.0.1') is v:true 67 | Expect composer#semver#compare('1.0.1', '>=', '8.0.1') is v:false 68 | Expect composer#semver#compare('1.0.1', '>=', '1.0.1') is v:true 69 | Expect composer#semver#compare('1', '>=', '1.6.10') is v:true 70 | end 71 | end 72 | 73 | " vim: fdm=marker:sw=2:sts=2:et 74 | -------------------------------------------------------------------------------- /t/utils.vim: -------------------------------------------------------------------------------- 1 | " t/utils.vim - Utility function tests 2 | " Maintainer: Noah Frederick 3 | 4 | call vspec#hint({'sid': 'composer#sid()'}) 5 | 6 | describe 's:throw()' 7 | it 'throws an exeption' 8 | Expect expr { vspec#call('s:throw', 'foo') } to_throw '^composer: foo$' 9 | end 10 | 11 | it 'sets v:errmsg' 12 | Expect expr { vspec#call('s:throw', 'foo') } to_throw '^composer: foo$' 13 | Expect v:errmsg ==# 'composer: foo' 14 | end 15 | 16 | it 'formats the message' 17 | Expect expr { vspec#call('s:throw', '(%s)', 'bar') } to_throw '^composer: (bar)$' 18 | end 19 | end 20 | 21 | describe 's:get_nested()' 22 | let g:dict = { 'foo': 'bar', 'baz': { 'x': 'a', 'y': 'b' } } 23 | 24 | it 'retrieves a key from a dict' 25 | Expect vspec#call('s:get_nested', g:dict, 'foo') ==# 'bar' 26 | end 27 | 28 | it 'retrieves a nested key from a dict' 29 | Expect vspec#call('s:get_nested', g:dict, 'baz.y') ==# 'b' 30 | end 31 | 32 | it 'returns an empty string' 33 | Expect vspec#call('s:get_nested', g:dict, 'nonexistent') ==# '' 34 | end 35 | 36 | it 'returns a default value' 37 | Expect vspec#call('s:get_nested', g:dict, 'nonexistent', 'z') ==# 'z' 38 | end 39 | end 40 | 41 | " vim: fdm=marker:sw=2:sts=2:et 42 | --------------------------------------------------------------------------------