Skip to content

Add require() helper #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ It's the Node equivalent of [Rails.vim (vimscript #1567)](https://github.com/tpo

This is just the first release to get the nodes rolling. If you've collected great helpers and shortcuts that help you work with Node, please share them via [email](mailto:[email protected]), [Twitter](https://twitter.com/theml) or [GitHub issues](https://github.com/moll/vim-node/issues) so we could incorporate them here, too! Thanks!

### Tour
### Basic functions

- Use `gf` on paths or requires to open the same file Node.js would.
- Use `gf` on `require(".")` to open `./index.js`
Expand All @@ -23,6 +23,37 @@ This is just the first release to get the nodes rolling. If you've collected gre
- Lets you even open Node's core modules. They're shown straight from Node's online repository without you having to download everything.
- Node.vim itself is tested with a thorough automated integration test suite! No cowboy coding here!

### `require()` helper

The plugin defines the commands `:Require` and `:Unrequire`, and aliases them
to `:R` and `:UR` if those command names are available. These commands will
manage the `require()` statements in a Node.js module for you, keeping the
statements sorted and aligned.

These commands assume that all `require()` statements appear in a single block
near the top of the file with no blank lines in between statements. They can
be used as follows:

# Add: async = require('async')
:Require async

# Add: somelib = require('./lib/somelib')
:Require ./lib/somelib

# Add: _ = require('lodash')
:Require _=lodash

# Add: MongoClient = require('mongodb').MongoClient
:Require MongoClient=mongodb.MongoClient
# or:
:Require mongodb.MongoClient

Those parameters will all work with the corresponding `:Unrequire` command, but
there, all you really need to specify is the module name (the part inside the
`require('...')` quotes).

### Improvements

Expect more to come soon and feel free to let me know what you're after!

PS. Node.vim is absolutely intended to work on Windows, but not yet tested there at all. If you could help, try it out and report issues, I'd be grateful!
Expand Down
282 changes: 282 additions & 0 deletions autoload/node.vim
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
let node#suffixesadd = [".js", ".json"]
let node#filetypes = ["javascript", "json"]

let node#coreModules = ["_debugger", "_http_agent", "_http_client",
\ "_http_common", "_http_incoming", "_http_outgoing", "_http_server",
\ "_linklist", "_stream_duplex", "_stream_passthrough", "_stream_readable",
\ "_stream_transform", "_stream_writable", "_tls_legacy", "_tls_wrap",
\ "assert", "buffer", "child_process", "cluster", "console", "constants",
\ "crypto", "dgram", "dns", "domain", "events", "freelist", "fs", "http",
\ "https", "module", "net", "node", "os", "path", "punycode", "querystring",
\ "readline", "repl", "smalloc", "stream", "string_decoder", "sys",
\ "timers", "tls", "tty", "url", "util", "vm", "zlib"]

function! node#initialize(root)
let b:node_root = a:root
call s:initializeCommands()
Expand All @@ -13,6 +23,19 @@ function! s:initializeCommands()
\ exe s:nedit(<q-args>, bufname("%"), "edit<bang>")
command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete Nopen
\ exe s:nopen(<q-args>, bufname("%"), "edit<bang>")
command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete Require
\ call s:require(<q-args>, 0)
command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete Unrequire
\ call s:require(<q-args>, 1)

if exists(":R") != 2
command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete R
\ call s:require(<q-args>, 0)
endif
if exists(":UR") != 2
command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete UR
\ call s:require(<q-args>, 1)
endif

nnoremap <buffer><silent> <Plug>NodeGotoFile
\ :call <SID>edit(expand("<cfile>"), bufname("%"))<CR>
Expand Down Expand Up @@ -70,6 +93,258 @@ function! s:nopen(name, from, ...)
if exists("b:node_root") | exe "lcd " . fnameescape(b:node_root) | endif
endfunction

function! s:require(expr, remove)
" TODO: Allow custom aliases?
" For example: :Require _ could expand to :Require _=lodash

" Remove extension if we were passed a completed filename
let expr = substitute(a:expr, '\v\.(js|json|coffee)$', "", "I")
" Remove trailing slashes if we were passed a completed folder name
let expr = substitute(expr, '\v/+$', "", "")

let pieces = split(expr, "=") " :Require XRegExp=xregexp
let varname = pieces[0] " XRegExp
let module = pieces[-1] " xregexp
let sortkey = module

if module =~# '\v^\.'
" For ./lib/whatever, sort on last piece of name
let sortkey = split(module, '/')[-1]
if varname ==# module
" No varname given, so default to last piece of name
let varname = sortkey
endif
let member = ""
else
" Treat mongodb.MongoClient as MongoClient=mongodb.MongoClient
if varname ==# module && module =~# '\v\.'
let varname = split(module, '\.')[-1]
endif
let pieces = split(module, '\.', 1)
let module = pieces[0] " mongodb
let member = join(pieces[1:], '\.') " MongoClient[.whatever]
endif

" If appropriate, search for this module in the package.json file and the
" node_modules folder, so we can warn the user about any problems.

let package = b:node_root . "/package.json"

if a:remove " Removing a module; no need to check package.json or node_modules

let showDepWarning = 0
let depWarningStr = ""

elseif module =~# '\v^\.' " Local module; no need to check package.json or node_modules

let showDepWarning = 0
let depWarningStr = ""

elseif index(g:node#coreModules, module) > -1 " Core module; same

let showDepWarning = 0
let depWarningStr = "core module"

else

" Check package.json for this module
if filereadable(package)
let deps = node#lib#deps(package)
if index(deps, module) > -1
let inPackage = 1
let inPackageStr = "in package.json"
else
let inPackage = 0
let inPackageStr = "not in package.json"
endif
else
let inPackage = 0
let inPackageStr = "package.json not found"
endif

" Check node_modules folder for this module
if isdirectory(b:node_root . "/node_modules/" . module)
let inFolder = 1
let inFolderStr = "in node_modules"
elseif isdirectory(b:node_root . "/node_modules")
let inFolder = 0
let inFolderStr = "not in node_modules"
else
let inFolder = 0
let inFolderStr = "node_modules folder not found"
endif

let showDepWarning = !inPackage || !inFolder
let depWarningStr = inFolderStr . ", " . inPackageStr

endif

" Find and store existing require() statements, and compute the position
" for the new require() statement

let inFile = 0
let inFileStr = "added to current file; "

" Set maximum varname width (for alignment)
if a:remove
let wmax = 0
else
let wmax = len(varname)
endif

let l = 1 " line index
let i = 0 " require index
let lbeg = 0 " beginning of require() block
let lend = 0 " end of require() block
let lmax = min([line('$'), 100]) " Assume that the require() block is in the first 100 lines
let rnew = -1 " position of the new require() statement
let reqs = [] " save the relevant values for all require() statements

while l <= lmax
let line = getline(l)

" Match current line
" Subpatterns:
" 1. dummy
" 2. varname
" 3. module
" 4. member
let m = matchlist(line, '\v'
\ . '^(\s+,?|\s*var)\s*'
\ . '([a-zA-Z0-9_]+)'
\ . '\s*\=\s*require\s*\([''"]'
\ . '([a-zA-Z0-9_./-]+)'
\ . '[''"]\s*\)'
\ . '(\.[a-zA-Z0-9_]+)?')
if len(m)

if !lbeg
let lbeg = l
endif

if !lend

let req = {
\ "varname" : m[2],
\ "module" : m[3],
\ "member" : m[4]
\ }

if req.module ==# module
let inFile = 1
let inFileStr = "already in current file, "
endif

let sorttest = req.module
if sorttest =~# '\v^\.'
let sorttest = split(sorttest, '/')[-1]
endif

if !a:remove && rnew == -1 && sorttest >? sortkey
let rnew = i
endif

if !a:remove || req.module !=# module
let wmax = max([len(req.varname), wmax])
call add(reqs, req)
let i += 1
endif

endif

else

if lbeg && !lend
let lend = l - 1
endif

endif

let l += 1
endwhile

if !lbeg
let lbeg = 1
let lend = 0
elseif !lend " TODO: does this ever happen?
let lend = lmax
if rnew == -1
let rnew = 0
endif
endif
if rnew == -1
let rnew = i
endif

if (!a:remove && !inFile) || (a:remove && inFile) " Need to insert or remove module

if !a:remove
call insert(reqs, {
\ "varname" : varname,
\ "module" : module,
\ "member" : member
\ }, rnew)
endif

let i = 0
" Turn the stored objects back into code text
" TODO: Support configurable styles? For example: no semicolons,
" commas before statements rather than after, no alignment, etc.
while i < len(reqs)
if i == 0
let line = "var "
else
let line = " "
endif
let line .= reqs[i].varname
let line .= repeat(" ", wmax - len(reqs[i].varname))
let line .= " = require('"
let line .= reqs[i].module
let line .= "')"
if len(reqs[i].member)
let line .= "." . reqs[i].member
endif
if i == len(reqs) - 1
let line .= ";"
else
let line .= ","
endif
let reqs[i] = line
let i += 1
endwhile

" Add an empty line if this is a new require() block and there will be
" something after it
if lbeg == 1 && lend == 0 && len(getline(1))
call add(reqs, '')
endif

" Save window position
let winview = winsaveview()
" Delete old require() block
if lend >= lbeg
execute 'silent! ' . lbeg . ',' . lend . 'delete _'
endif
" Insert new require() block
call append(lbeg - 1, reqs)
" Adjust saved window position for changed number of lines
let offset = len(reqs) - (lend - lbeg + 1)
let winview.lnum += offset
if winview.topline > 1
let winview.topline = max([1, winview.topline + offset])
endif
" Restore window position
call winrestview(winview)

endif

if !a:remove && (inFile || showDepWarning)
call s:warning("Module '" . module . "': " . inFileStr . depWarningStr)
elseif a:remove && !inFile
call s:warning("Module '" . module . "': not in current file")
endif
endfunction

function! s:complete(arg, cmd, cursor)
let matches = node#lib#glob(s:dirname(a:arg))

Expand Down Expand Up @@ -102,3 +377,10 @@ function! s:error(msg)
echohl NONE
let v:errmsg = a:msg
endfunction

function! s:warning(msg)
echohl WarningMsg
echomsg a:msg
echohl NONE
let v:warningmsg = a:msg
endfunction
Loading