diff --git a/README.md b/README.md index e3a3bd0..b4aba49 100644 --- a/README.md +++ b/README.md @@ -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:andri@dot.ee), [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` @@ -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! diff --git a/autoload/node.vim b/autoload/node.vim index 287344c..cafddea 100644 --- a/autoload/node.vim +++ b/autoload/node.vim @@ -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() @@ -13,6 +23,19 @@ function! s:initializeCommands() \ exe s:nedit(, bufname("%"), "edit") command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete Nopen \ exe s:nopen(, bufname("%"), "edit") + command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete Require + \ call s:require(, 0) + command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete Unrequire + \ call s:require(, 1) + + if exists(":R") != 2 + command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete R + \ call s:require(, 0) + endif + if exists(":UR") != 2 + command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete UR + \ call s:require(, 1) + endif nnoremap NodeGotoFile \ :call edit(expand(""), bufname("%")) @@ -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)) @@ -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 diff --git a/autoload/node/lib.vim b/autoload/node/lib.vim index 702bee3..b7a02a7 100644 --- a/autoload/node/lib.vim +++ b/autoload/node/lib.vim @@ -4,18 +4,9 @@ let s:MODULE = '\v^(/|\.\.?(/|$))@!' " Damn Netrw can't handle HTTPS at all. It's 2013! Insecure bastard! let s:CORE_URL_PREFIX = "http://rawgithub.com/joyent/node" -let s:CORE_MODULES = ["_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#lib#find(name, from) - if index(s:CORE_MODULES, a:name) != -1 + if index(g:node#coreModules, a:name) != -1 let l:version = node#lib#version() let l:version = empty(l:version) ? "master" : "v" . l:version let l:dir = a:name == "node" ? "src" : "lib" @@ -86,6 +77,21 @@ function! s:mainFromPackage(path) endfor endfunction +function! node#lib#deps(path) + let inDeps = 0 + let deps = [] + for line in readfile(a:path) + if line =~# '\v^\s*"(devDependencies|dependencies)"\s*:\s*\{\s*$' + let inDeps = 1 + elseif line =~# '^\s*}\s*,?\s*$' + let inDeps = 0 + elseif inDeps + call add(deps, matchstr(line, '^\s*"\zs[^"]\+\ze"')) + endif + endfor + return deps +endfunction + function! s:resolveSuffix(path) for suffix in s:uniq([""] + g:node#suffixesadd + split(&l:suffixesadd, ",")) let path = a:path . suffix @@ -99,7 +105,7 @@ function! node#lib#glob(name) let matches = [] if empty(a:name) - let matches += s:CORE_MODULES + let matches += g:node#coreModules endif if empty(a:name) || a:name =~# s:MODULE