diff --git a/README.md b/README.md index c494516..9963aaa 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Vim support for [Laravel/Lumen][laravel] projects. * Navigation commands such as `:Econtroller`, `:Eroutes`, `:Etest` and [many more][wiki-navigation]. * Enhanced `gf` command works on class names, template names, config and translation keys. * Complete view/route names in insert mode. +* Interact with a Homestead guest VM from the host machine using `:Homestead`. * Use `:Console` to fire up a REPL (`artisan tinker`). * Use `:Start` to serve the app locally (`artisan serve`). diff --git a/autoload/laravel.vim b/autoload/laravel.vim index d9abd78..250dd89 100644 --- a/autoload/laravel.vim +++ b/autoload/laravel.vim @@ -149,8 +149,20 @@ function! s:app_views_path(...) dict abort return join([self._root, 'resources/views'] + a:000, '/') endfunction +"" +" Get path to source root on the Homestead VM, optionally with [path] appended. +function! s:app_homestead_path(...) dict abort + if self.cache.needs('homestead_root') + call self.cache.set('homestead_root', laravel#homestead#root(self._root)) + endif + + let root = self.cache.get('homestead_root') + + return empty(root) ? '' : join([root] + a:000, '/') +endfunction + call s:add_methods('app', ['glob', 'has_dir', 'has_file', 'has_path']) -call s:add_methods('app', ['path', 'src_path', 'config_path', 'migrations_path', 'find_migration', 'expand_migration', 'views_path']) +call s:add_methods('app', ['path', 'src_path', 'config_path', 'migrations_path', 'find_migration', 'expand_migration', 'views_path', 'homestead_path']) "" " Detect app's namespace @@ -565,6 +577,46 @@ function! laravel#buffer_commands() abort " Invoke Artisan with [arguments] (with intelligent completion). command! -buffer -bang -bar -nargs=* -complete=customlist,laravel#artisan#complete \ Artisan execute laravel#artisan#exec(, ) + + "" + " @command Homestead {cmd} + " Invoke shell {cmd} on the Homestead VM over SSH. + " + " Several strategies for executing the ssh command in order: + " + " * Dispatch's |:Start| command + " * The built-in |:terminal| + " * At Vim's command line via |:!| + " + " The {cmd} is executed with the working directory being the project's + " directory on the VM. The project's directory is detected from your + " Homestead.json configuration file, using the "folders" mappings to do the + " translation from host path to guest path: > + " "folders": [ + " { + " "map": "~/code", + " "to": "/home/vagrant/code" + " } + " ], + " < + " + " The plug-in will look for the Homestead.json file in the directory + " specified in @setting(laravel_homestead_dir) or in ~/Homestead. + " + " Note: Only Homestead.json is taken into account, and not Homestead.yaml, + " since Vim cannot parse YAML. If you prefer to use the Homestead.yaml file, + " it's sufficient to set only the "folders" array in Homestead.json. + " + " @command Homestead + " Start an interactive SSH session on the Homestead VM. + " + " @command Homestead! [arguments] + " Invoke Vagrant with [arguments] in the context of the Homestead directory + " on the host machine. For example, to start the VM: > + " :Homestead! up + " < + command! -buffer -bang -bar -nargs=* -complete=shellcmd + \ Homestead execute laravel#homestead#exec(, ) endfunction "" diff --git a/autoload/laravel/homestead.vim b/autoload/laravel/homestead.vim new file mode 100644 index 0000000..6124b8a --- /dev/null +++ b/autoload/laravel/homestead.vim @@ -0,0 +1,130 @@ +" autoload/laravel/homestead.vim - Laravel Homestead support for Vim +" Maintainer: Noah Frederick + +"" +" The directory where Homestead is installed. +let s:dir = get(g:, 'laravel_homestead_dir', '~/Homestead') +let s:yaml = s:dir . '/Homestead.yaml' +let s:json = s:dir . '/Homestead.json' + +"" +" Get Dict from JSON {expr}. +function! s:json_decode(expr) abort + try + if exists('*json_decode') + let expr = type(a:expr) == type([]) ? join(a:expr, "\n") : a:expr + return json_decode(expr) + else + return projectionist#json_parse(a:expr) + endif + catch /^Vim\%((\a\+)\)\=:E474/ + call laravel#error('Homestead.json cannot be parsed') + catch /^invalid JSON/ + call laravel#error('Homestead.json cannot be parsed') + catch /^Vim\%((\a\+)\)\=:E117/ + call laravel#error('projectionist is not available') + endtry + return {} +endfunction + +"" +" Get path to current project on the Homestead VM. +function! laravel#homestead#root(app_root) abort + if !filereadable(s:json) + call laravel#error('Homestead.json cannot be read: ' + \ . s:json . ' (set g:laravel_homestead_dir)') + return '' + endif + + let config = s:json_decode(readfile(s:json)) + + for folder in get(config, 'folders', []) + let source = expand(folder.map) + + if a:app_root . '/' =~# '^' . source . '/' + return substitute(a:app_root, '^' . source, folder.to, '') + endif + endfor + + return '' +endfunction + +"" +" Change working directory to {dir}, respecting current window's local dir +" state. Returns old working directory to be restored later by a second +" invocation of the function. +function! s:cd(dir) abort + let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' + let cwd = getcwd() + execute cd fnameescape(a:dir) + return cwd +endfunction + +"" +" Build SSH shell command from command-line arguments. +function! s:ssh(args) abort + if empty(a:args) + return 'vagrant ssh' + endif + + let root = laravel#app().homestead_path() + + if empty(root) + call laravel#error('Homestead site not configured for ' + \ . laravel#app().path()) + return '' + endif + + let args = insert(a:args, 'cd ' . fnamemodify(root, ':S') . ' &&') + return 'vagrant ssh -- ' . shellescape(join(args)) +endfunction + +"" +" Build Vagrant shell command from command-line arguments. +function! s:vagrant(args) abort + let args = empty(a:args) ? ['status'] : a:args + return 'vagrant ' . join(args) +endfunction + +"" +" The :Homestead command. +function! laravel#homestead#exec(...) abort + let args = copy(a:000) + let vagrant = remove(args, 0) + + if !isdirectory(s:dir) + return laravel#error('Homestead directory does not exist: ' + \ . s:dir . ' (set g:laravel_homestead_dir)') + endif + + let cmdline = vagrant ==# '!' ? s:vagrant(args) : s:ssh(args) + + if empty(cmdline) + " There is no path configured for the VM. + return '' + endif + + if exists(':Start') + execute 'Start -title=homestead -wait=always -dir='.fnameescape(s:dir) cmdline + elseif exists(':terminal') + tab split + execute 'lcd' fnameescape(s:dir) + execute 'terminal' cmdline + else + let cwd = s:cd(s:dir) + execute '!' . cmdline + call s:cd(cwd) + endif + + return '' +endfunction + +"" +" @private +" Hack for testing script-local functions. +function! laravel#homestead#sid() + nnoremap + return maparg('', 'n') +endfunction + +" vim: fdm=marker:sw=2:sts=2:et diff --git a/doc/laravel.txt b/doc/laravel.txt index e5f773e..0def5f9 100644 --- a/doc/laravel.txt +++ b/doc/laravel.txt @@ -4,8 +4,9 @@ Noah Frederick *Laravel.vim* *laravel* ============================================================================== CONTENTS *laravel-contents* 1. Introduction..............................................|laravel-intro| - 2. Commands...............................................|laravel-commands| - 3. About.....................................................|laravel-about| + 2. Configuration............................................|laravel-config| + 3. Commands...............................................|laravel-commands| + 4. About.....................................................|laravel-about| ============================================================================== INTRODUCTION *laravel-intro* @@ -15,16 +16,67 @@ Some features include: * The |:Artisan| command wraps artisan with intelligent completion. * Includes projections for projectionist.vim. + * Use |:Homestead| to send commands over SSH to your development VM. * Use |:Console| to fire up a REPL (artisan tinker). This plug-in is only available if 'compatible' is not set. +============================================================================== +CONFIGURATION *laravel-config* + + *g:laravel_homestead_dir* +The directory where Homestead is installed. Default: +> + '~/Homestead' +< + ============================================================================== COMMANDS *laravel-commands* :Artisan[!] [arguments] *:Artisan* Invoke Artisan with [arguments] (with intelligent completion). +:Homestead {cmd} *:Homestead* + Invoke shell {cmd} on the Homestead VM over SSH. + + Several strategies for executing the ssh command in order: + + * Dispatch's |:Start| command + * The built-in |:terminal| + * At Vim's command line via |:!| + + The {cmd} is executed with the working directory being the project's + directory on the VM. The project's directory is detected from your + Homestead.json configuration file, using the "folders" mappings to do the + translation from host path to guest path: +> + "folders": [ + { + "map": "~/code", + "to": "/home/vagrant/code" + } + ], +< + + The plug-in will look for the Homestead.json file in the directory specified + in |g:laravel_homestead_dir| or in ~/Homestead. + + Note: Only Homestead.json is taken into account, and not Homestead.yaml, + since Vim cannot parse YAML. If you prefer to use the Homestead.yaml file, + it's sufficient to set only the "folders" array in Homestead.json. + + +:Homestead + Start an interactive SSH session on the Homestead VM. + + +:Homestead! [arguments] + Invoke Vagrant with [arguments] in the context of the Homestead directory on + the host machine. For example, to start the VM: +> + :Homestead! up +< + ============================================================================== ABOUT *laravel-about* diff --git a/plugin/laravel.vim b/plugin/laravel.vim index 69d8c6e..05d2e72 100644 --- a/plugin/laravel.vim +++ b/plugin/laravel.vim @@ -9,10 +9,17 @@ " " * The |:Artisan| command wraps artisan with intelligent completion. " * Includes projections for projectionist.vim. +" * Use |:Homestead| to send commands over SSH to your development VM. " * Use |:Console| to fire up a REPL (artisan tinker). " " This plug-in is only available if 'compatible' is not set. +"" +" @setting g:laravel_homestead_dir +" The directory where Homestead is installed. Default: > +" '~/Homestead' +" < + "" " @section About, about " @plugin(stylized) is distributed under the same terms as Vim itself (see diff --git a/test/homestead.vader b/test/homestead.vader new file mode 100644 index 0000000..4dc44a1 --- /dev/null +++ b/test/homestead.vader @@ -0,0 +1,20 @@ +Before (in a laravel buffer): + let g:laravel_homestead_dir = expand('test/fixtures') + tabedit test/fixtures/laravel-8/.env + +After (clean up buffer): + bdelete + +Execute (Detect Homestead app root): + AssertEqual laravel#homestead#root('/home/local/code/project'), '/home/vagrant/code/project' + +Execute (Invalid Homestead app root): + AssertEqual laravel#homestead#root('/home/local/invalid/project'), '' + +Execute (Access Homestead app root via app object): + " Fake the app root. + let b:app = deepcopy(laravel#app()) + let b:app._root = '/home/local/code/project' + + AssertEqual b:app.homestead_path(), '/home/vagrant/code/project' + AssertEqual b:app.homestead_path('public'), '/home/vagrant/code/project/public'