" Vim completion script
|
|
" Language: Javascript(node)
|
|
" Maintainer: Lin Zhang ( myhere.2009 AT gmail DOT com )
|
|
" Last Change: 2012-8-18 1:32:00
|
|
|
|
" save current dir
|
|
let s:nodejs_doc_file = expand('<sfile>:p:h') . '/nodejs-doc.vim'
|
|
|
|
let s:js_varname_reg = '[$a-zA-Z_][$a-zA-Z0-9_]*'
|
|
|
|
let s:js_obj_declare_type = {
|
|
\ 'global': 0,
|
|
\ 'require': 1,
|
|
\ 'constructor': 2
|
|
\ }
|
|
|
|
" settings
|
|
" default setting
|
|
let s:nodejs_complete_config = {
|
|
\ 'js_compl_fn': 'javascriptcomplete#CompleteJS',
|
|
\ 'max_node_compl_len': 15
|
|
\}
|
|
if exists('g:nodejs_complete_config') && type(g:nodejs_complete_config) == type({})
|
|
let g:nodejs_complete_config = extend(s:nodejs_complete_config, g:nodejs_complete_config)
|
|
else
|
|
let g:nodejs_complete_config = s:nodejs_complete_config
|
|
endif
|
|
unlet s:nodejs_complete_config
|
|
|
|
function! nodejscomplete#CompleteJS(findstart, base)"{{{
|
|
if a:findstart
|
|
try
|
|
let JS_compl_fn = function(g:nodejs_complete_config.js_compl_fn)
|
|
let start = call(JS_compl_fn, [a:findstart, a:base])
|
|
catch /.*/
|
|
echo '!!!!!!!!!!function [' . g:nodejs_complete_config.js_compl_fn . '] is not exists!'
|
|
endtry
|
|
|
|
"Decho 'start: ' . start
|
|
" str[start: end] end 为负数时从末尾截取
|
|
if start - 1 < 0
|
|
let b:nodecompl_context = ''
|
|
else
|
|
let line = getline('.')
|
|
let b:nodecompl_context = line[:start-1]
|
|
endif
|
|
return start
|
|
else
|
|
let posi = getpos('.')
|
|
let result = s:getNodeComplete(a:base, b:nodecompl_context)
|
|
" the function above will move the cursor
|
|
" so we restore the cursor position
|
|
" JS_compl_fn below may rely on the cursor position
|
|
call setpos('.', posi)
|
|
|
|
"Decho 'nodecomplete: ' . string(result)
|
|
unlet b:nodecompl_context
|
|
|
|
let nodejs_compl = result.complete
|
|
" limit nodejs complete count
|
|
if g:nodejs_complete_config.max_node_compl_len != 0
|
|
let nodejs_compl = nodejs_compl[0 : g:nodejs_complete_config.max_node_compl_len - 1]
|
|
endif
|
|
|
|
if result.continue
|
|
try
|
|
let JS_compl_fn = function(g:nodejs_complete_config.js_compl_fn)
|
|
let js_compl = call(JS_compl_fn, [a:findstart, a:base])
|
|
catch /.*/
|
|
echo '!!!!!!!!!!function [' . g:nodejs_complete_config.js_compl_fn . '] is not exists!'
|
|
endtry
|
|
|
|
"Decho 'js_compl: ' . string(js_compl)
|
|
|
|
return nodejs_compl + js_compl
|
|
else
|
|
return nodejs_compl
|
|
endif
|
|
endif
|
|
endfunction"}}}
|
|
|
|
" get complete
|
|
function! s:getNodeComplete(base, context)"{{{
|
|
"Decho 'base: ' . a:base
|
|
"Decho 'context: ' . a:context
|
|
|
|
" TODO: 排除 module.property.h 情况
|
|
let mod_reg = '\(' . s:js_varname_reg . '\)\s*\(\.\|\[\s*["'']\?\)\s*$'
|
|
let matched = matchlist(a:context, mod_reg)
|
|
"Decho 'mod_reg: ' . mod_reg
|
|
|
|
" 模块属性补全
|
|
if len(matched) > 0
|
|
let var_name = matched[1]
|
|
let operator = matched[2]
|
|
let position = [line('.'), len(a:context) - len(matched[0])]
|
|
"Decho 'var_name: ' . var_name . ' ; operator: ' . operator
|
|
let declare_info = s:getObjDeclareInfo(var_name, position)
|
|
"Decho 'mod_info: ' . string(declare_info) . '; compl_prefix: ' . a:base
|
|
|
|
let compl_list = s:getObjectComplete(declare_info.type, declare_info.value,
|
|
\ a:base, operator)
|
|
|
|
let ret = {
|
|
\ 'complete': compl_list
|
|
\ }
|
|
if len(compl_list) == 0
|
|
let ret.continue = 1
|
|
else
|
|
let ret.continue = 0
|
|
endif
|
|
" 全局补全
|
|
else
|
|
"Decho 'var complete'
|
|
let ret = {
|
|
\ 'continue': 1,
|
|
\ 'complete': s:getVariableComplete(a:context, a:base)
|
|
\ }
|
|
endif
|
|
|
|
return ret
|
|
endfunction"}}}
|
|
|
|
function! s:getObjDeclareInfo(var_name, position)"{{{
|
|
let position = s:fixPosition(a:position)
|
|
"Decho 'position: ' . string(position)
|
|
|
|
if position[0] <= 0
|
|
return {
|
|
\ 'type': s:js_obj_declare_type.global,
|
|
\ 'value': a:var_name
|
|
\}
|
|
endif
|
|
|
|
let decl_stmt_prefix_reg = '\<' . a:var_name . '\_s*=\_s*'
|
|
" search backward, don't move the cursor, don't wrap
|
|
call cursor(position[0], position[1])
|
|
let begin_position = searchpos(decl_stmt_prefix_reg, 'bnW')
|
|
if begin_position[0] == 0
|
|
return {
|
|
\ 'type': s:js_obj_declare_type.global,
|
|
\ 'value': a:var_name
|
|
\}
|
|
endif
|
|
|
|
" make sure it's not in comments...
|
|
if !s:isDeclaration(begin_position)
|
|
return s:getObjDeclareInfo(a:var_name, begin_position)
|
|
endif
|
|
|
|
let lines = s:getLinesInRange(begin_position, position)
|
|
"Decho 'lines: ' . string(lines)
|
|
let code = join(lines, "\n")
|
|
|
|
" require
|
|
let require_stmt_reg = decl_stmt_prefix_reg .
|
|
\ 'require\_s*(\_s*\([''"]\)\zs[^)''"]\+\ze\1\_s*)'
|
|
let matched = matchstr(code, require_stmt_reg)
|
|
if len(matched)
|
|
return {
|
|
\ 'type': s:js_obj_declare_type.require,
|
|
\ 'value': matched
|
|
\}
|
|
endif
|
|
|
|
" new
|
|
let new_stmt_reg = decl_stmt_prefix_reg .
|
|
\ 'new\_s\+\zs' . s:js_varname_reg . '\%(\_s*\.\_s*' .
|
|
\ s:js_varname_reg . '\)*\ze'
|
|
let matched = matchstr(code, new_stmt_reg)
|
|
if len(matched)
|
|
let parts = split(matched, '\.')
|
|
return {
|
|
\ 'type': s:js_obj_declare_type.constructor,
|
|
\ 'value': [s:getObjDeclareInfo(parts[0], begin_position), join(parts[1:], '.')]
|
|
\}
|
|
endif
|
|
|
|
" new
|
|
" var emitter = new (require('events')).EventEmitter;
|
|
let new_stmt_reg = decl_stmt_prefix_reg .
|
|
\ 'new\_s\+' .
|
|
\ '(\_s*require\_s*(\([''"]\)\(' . s:js_varname_reg . '\)\1\_s*)\_s*)' .
|
|
\ '\_s*' .
|
|
\ '\(\%(\.' . s:js_varname_reg . '\)\+\)'
|
|
|
|
let matchedList = matchlist(code, new_stmt_reg)
|
|
if (len(matchedList))
|
|
"Decho 'new stmt: ' . string(matchedList)
|
|
let props = [matchedList[3][1:]] + matchedList[4:]
|
|
"Decho 'props: ' . string(props)
|
|
return {
|
|
\ 'type': s:js_obj_declare_type.constructor,
|
|
\ 'value': [
|
|
\ {
|
|
\ 'type': s:js_obj_declare_type.require,
|
|
\ 'value': matchedList[2]
|
|
\ },
|
|
\ join(props, '')
|
|
\ ]
|
|
\}
|
|
endif
|
|
|
|
|
|
" assign
|
|
let assign_stmt_reg = decl_stmt_prefix_reg . '\zs' . s:js_varname_reg . '\ze'
|
|
let matched = matchstr(code, assign_stmt_reg)
|
|
if len(matched)
|
|
return s:getObjDeclareInfo(a:var_name, begin_position)
|
|
endif
|
|
|
|
" continure to search backward
|
|
return s:getObjDeclareInfo(a:var_name, begin_position)
|
|
endfunction"}}}
|
|
|
|
function! s:isDeclaration(position)"{{{
|
|
let [line_num, col_num] = a:position
|
|
" syntaxName @see: $VIMRUNTIME/syntax/javascript.vim
|
|
let syntaxName = synIDattr(synID(line_num, col_num, 0), 'name')
|
|
if syntaxName =~ '^javaScript\%(Comment\|LineComment\|String\|RegexpString\)'
|
|
return 0
|
|
else
|
|
return 1
|
|
endif
|
|
endfunction"}}}
|
|
|
|
" only complete nodejs's module info
|
|
function! s:getObjectComplete(type, mod_name, prop_name, operator)"{{{
|
|
" new
|
|
if a:type == s:js_obj_declare_type.constructor
|
|
let list = s:getConstructedObjectComplete(a:mod_name)
|
|
" require and global
|
|
else
|
|
let list = s:getNodeDocList(a:type, a:mod_name, 'props')
|
|
endif
|
|
|
|
if !len(list)
|
|
return list
|
|
else
|
|
" no prop_name suplied
|
|
if (len(a:prop_name) == 0)
|
|
let ret = list
|
|
else
|
|
let ret = s:smartFilter(list, 'v:val["word"]', a:prop_name)
|
|
endif
|
|
|
|
let [prefix, suffix] = ['', '']
|
|
let matched = matchlist(a:operator, '\[\s*\(["'']\)\?')
|
|
"Decho 'operator_matched: ' . string(matched)
|
|
if len(matched)
|
|
if len(matched[1])
|
|
let [prefix, suffix] = ['', matched[1] . ']']
|
|
else
|
|
let [prefix, suffix] = ['''', ''']']
|
|
endif
|
|
endif
|
|
|
|
for item in ret
|
|
let item.word = prefix . item.word . suffix
|
|
endfor
|
|
call s:addFunctionParen(ret)
|
|
|
|
return ret
|
|
endif
|
|
endfunction"}}}
|
|
|
|
function! s:getVariableComplete(context, var_name)"{{{
|
|
"Decho 'var_name: ' . a:var_name
|
|
|
|
" complete require's arguments
|
|
let matched = matchlist(a:context, 'require\s*(\s*\%(\([''"]\)\(\.\{1,2}.*\)\=\)\=$')
|
|
if (len(matched) > 0)
|
|
"Decho 'require complete: ' . string(matched)
|
|
|
|
if (len(matched[2]) > 0) " complete -> require('./
|
|
let mod_names = s:getModuleInCurrentDir(a:context, a:var_name, matched)
|
|
else
|
|
let mod_names = s:getModuleNames()
|
|
|
|
if (len(matched[1]) == 0) " complete -> require(
|
|
call map(mod_names, '"''" . v:val . "'')"')
|
|
elseif (len(a:var_name) == 0) " complete -> require('
|
|
call map(mod_names, 'v:val . "' . escape(matched[1], '"') . ')"')
|
|
else " complete -> require('ti
|
|
let mod_names = filter(mod_names, 'v:val =~# "^' . a:var_name . '"')
|
|
call map(mod_names, 'v:val . "' . escape(matched[1], '"') . ')"')
|
|
endif
|
|
endif
|
|
|
|
return mod_names
|
|
endif
|
|
|
|
" complete global variables
|
|
let vars = []
|
|
if (len(a:var_name) == 0)
|
|
return vars
|
|
endif
|
|
|
|
call s:loadNodeDocData()
|
|
|
|
if (has_key(g:nodejs_complete_data, 'vars'))
|
|
let vars = deepcopy(g:nodejs_complete_data.vars)
|
|
endif
|
|
|
|
let ret = s:smartFilter(vars, 'v:val["word"]', a:var_name)
|
|
|
|
call s:addFunctionParen(ret)
|
|
|
|
return ret
|
|
endfunction"}}}
|
|
|
|
function! s:getModuleInCurrentDir(context, var_name, matched)"{{{
|
|
let mod_names = []
|
|
let path = a:matched[2] . a:var_name
|
|
|
|
" typed as require('..
|
|
" complete as require('../
|
|
" cause the latter one is more common
|
|
let compl_prefix = ''
|
|
if (path =~# '\.\.$')
|
|
let compl_prefix = '/'
|
|
let path = path . compl_prefix
|
|
endif
|
|
|
|
"Decho 'path: ' . path
|
|
|
|
let current_dir = expand('%:p:h')
|
|
let glob_path = current_dir . '/' . path . '*'
|
|
let files = s:fuzglob(glob_path)
|
|
"Decho 'glob: ' . glob_path
|
|
"Decho 'current dir files: ' . string(files)
|
|
for file in files
|
|
" not '.' and '..'
|
|
if ((isdirectory(file) ) || file =~? '\.json$\|\.js$')
|
|
let mod_file = file
|
|
" directory
|
|
if (file !~? '\.json$\|\.js$')
|
|
let mod_file = mod_file . '/'
|
|
endif
|
|
|
|
" get complete word
|
|
let mod_file = substitute(mod_file, '\', '/', 'g')
|
|
let start = len(glob_path) - 1 " substract character '*'
|
|
let compl_infix = strpart(mod_file, start)
|
|
"Decho 'idx: ' . start
|
|
"Decho 'compl_infix: ' . compl_infix
|
|
"Decho 'relative file: ' . mod_file
|
|
|
|
let mod_name = compl_prefix . a:var_name . compl_infix
|
|
" file module, not a directory
|
|
if (compl_infix !~# '/$')
|
|
let mod_name = mod_name . a:matched[1] . ')'
|
|
endif
|
|
|
|
"Decho 'mod_name: ' . mod_name
|
|
call add(mod_names, mod_name)
|
|
endif
|
|
endfor
|
|
|
|
"Decho 'relative path: ' . path
|
|
|
|
return mod_names
|
|
endfunction"}}}
|
|
|
|
function! s:getModuleNames()"{{{
|
|
call s:loadNodeDocData()
|
|
|
|
let mod_names = []
|
|
|
|
" build-in module name
|
|
if (has_key(g:nodejs_complete_data, 'modules'))
|
|
let mod_names = keys(g:nodejs_complete_data.modules)
|
|
endif
|
|
|
|
|
|
" find module in 'module_dir' folder
|
|
if (!exists('b:npm_module_names'))
|
|
let current_dir = expand('%:p:h')
|
|
|
|
let b:npm_module_names = s:getModuleNamesInNode_modulesFolder(current_dir)
|
|
endif
|
|
|
|
let mod_names = mod_names + b:npm_module_names
|
|
|
|
return sort(mod_names)
|
|
endfunction"}}}
|
|
|
|
function! s:getModuleNamesInNode_modulesFolder(current_dir)"{{{
|
|
" ensure platform coincidence
|
|
let base_dir = substitute(a:current_dir, '\', '/', 'g')
|
|
"Decho 'base_dir: ' . base_dir
|
|
|
|
let ret = []
|
|
|
|
let parts = split(base_dir, '/', 1)
|
|
"Decho 'parts: ' . string(parts)
|
|
let idx = 0
|
|
let len = len(parts)
|
|
let sub_parts = []
|
|
while idx < len
|
|
let sub_parts = add(sub_parts, parts[idx])
|
|
let module_dir = join(sub_parts, '/') . '/node_modules'
|
|
"Decho 'directory: ' . module_dir
|
|
|
|
if (isdirectory(module_dir))
|
|
let files = s:fuzglob(module_dir . '/*')
|
|
"Decho 'node_module files: ' . string(files)
|
|
for file in files
|
|
if (isdirectory(file) || file =~? '\.json$\|\.js$')
|
|
let mod_name = matchstr(file, '[^/\\]\+$')
|
|
let ret = add(ret, mod_name)
|
|
endif
|
|
endfor
|
|
endif
|
|
|
|
let idx = idx + 1
|
|
endwhile
|
|
|
|
"Decho 'npm modules: ' . string(ret)
|
|
|
|
return ret
|
|
endfunction"}}}
|
|
|
|
function! s:getConstructedObjectComplete(constructor_info)"{{{
|
|
"Decho 'getConstructedObjectComplete, constructor_info: ' . string(a:constructor_info)
|
|
|
|
let ret = []
|
|
|
|
let [declare_info, class_name] = a:constructor_info
|
|
let mod_name = declare_info.value
|
|
" global
|
|
if declare_info.type == s:js_obj_declare_type.global
|
|
" Buffer
|
|
if class_name == ''
|
|
let class_name = '.self'
|
|
endif
|
|
" global.Buffer
|
|
if mod_name == 'global'
|
|
let mod_name = class_name
|
|
let class_name = '.self'
|
|
endif
|
|
endif
|
|
|
|
" global or require
|
|
if declare_info.type == s:js_obj_declare_type.global ||
|
|
\ declare_info.type == s:js_obj_declare_type.require
|
|
|
|
let ret = s:getNodeDocList(declare_info.type, mod_name, 'classes', class_name)
|
|
endif
|
|
|
|
return ret
|
|
endfunction"}}}
|
|
|
|
function! s:addFunctionParen(compl_list)"{{{
|
|
for item in a:compl_list
|
|
if type(item) == 4
|
|
if item.kind == 'f'
|
|
let item.word = item.word . '('
|
|
endif
|
|
endif
|
|
endfor
|
|
|
|
return a:compl_list
|
|
endfunction"}}}
|
|
|
|
function! s:loadNodeDocData()"{{{
|
|
" load node module data
|
|
if (!exists('g:nodejs_complete_data'))
|
|
" load data from external file
|
|
let filename = s:nodejs_doc_file
|
|
"Decho 'filename: ' . filename
|
|
if (filereadable(filename))
|
|
execute 'so ' . filename
|
|
"Decho string(g:nodejs_complete_data)
|
|
else
|
|
"Decho 'not readable: ' . filename
|
|
endif
|
|
endif
|
|
endfunction"}}}
|
|
|
|
" get infomation from g:nodejs_complete_data
|
|
" @param mod_type {Enum}
|
|
" @param mod_name {String}
|
|
" @param type {Enum} 'props' | 'classes'
|
|
" @param {String} if type == 'classes', then it exists and is class_name
|
|
" else do not exist
|
|
function! s:getNodeDocList(mod_type, mod_name, type, ...)"{{{
|
|
call s:loadNodeDocData()
|
|
|
|
if a:mod_type == s:js_obj_declare_type.require
|
|
let type = 'modules'
|
|
else
|
|
let type = 'globals'
|
|
endif
|
|
|
|
if (has_key(g:nodejs_complete_data[type], a:mod_name))
|
|
let mod = g:nodejs_complete_data[type][a:mod_name]
|
|
else
|
|
let mod = {}
|
|
endif
|
|
|
|
" class
|
|
if a:0 != 0
|
|
let class_name = a:1
|
|
if (has_key(mod, a:type))
|
|
let classes = mod[a:type]
|
|
else
|
|
let classes = {}
|
|
endif
|
|
|
|
if (has_key(classes, a:1))
|
|
let ret = classes[class_name]
|
|
else
|
|
let ret = []
|
|
endif
|
|
" property
|
|
else
|
|
if (has_key(mod, a:type))
|
|
let ret = mod[a:type]
|
|
else
|
|
let ret = []
|
|
endif
|
|
endif
|
|
|
|
return deepcopy(ret)
|
|
endfunction"}}}
|
|
|
|
" copied from FuzzyFinder/autoload/fuf.vim
|
|
" returns list of paths.
|
|
" An argument for glob() is normalized in order to avoid a bug on Windows.
|
|
function! s:fuzglob(expr)"{{{
|
|
" Substitutes "\", because on Windows, "**\" doesn't include ".\",
|
|
" but "**/" include "./". I don't know why.
|
|
return split(glob(substitute(a:expr, '\', '/', 'g')), "\n")
|
|
endfunction"}}}
|
|
|
|
" when x <= 0, return [0, 0]
|
|
" when y <= 0, move to previous line end
|
|
function! s:fixPosition(position)"{{{
|
|
let [x, y] = a:position
|
|
|
|
if x <= 0
|
|
return [0, 0]
|
|
endif
|
|
|
|
if y <= 0
|
|
let x -= 1
|
|
let y = len(getline(x))
|
|
|
|
return s:fixPosition([x, y])
|
|
endif
|
|
|
|
return [x, y]
|
|
endfunction"}}}
|
|
|
|
" return a List contains every line
|
|
function! s:getLinesInRange(begin_position, end_position)"{{{
|
|
let [begin_x, begin_y] = a:begin_position
|
|
let [end_x, end_y] = a:end_position
|
|
|
|
let lines = []
|
|
if begin_x == end_x
|
|
let line = getline(begin_x)
|
|
call add(lines, line[begin_y - 1 : end_y - 1])
|
|
else
|
|
let line = getline(begin_x)
|
|
call add(lines, line[begin_y - 1 :])
|
|
|
|
let x = begin_x + 1
|
|
while x < end_x
|
|
let line = getline(x)
|
|
call add(lines, line)
|
|
let x += 1
|
|
endwhile
|
|
|
|
let line = getline(end_x)
|
|
call add(lines, line[: end_y - 1])
|
|
endif
|
|
|
|
return lines
|
|
endfunction"}}}
|
|
|
|
" filter items with exact match at first
|
|
function! s:smartFilter(items, str, keyword)"{{{
|
|
let items = filter(a:items, a:str . ' =~ "' . a:keyword . '"')
|
|
let [exact_ret, fuzzy_ret] = [[], []]
|
|
for item in items
|
|
if item.word =~ '^' . a:keyword
|
|
call add(exact_ret, item)
|
|
else
|
|
call add(fuzzy_ret, item)
|
|
endif
|
|
endfor
|
|
|
|
return exact_ret + fuzzy_ret
|
|
endfunction"}}}
|
|
|
|
"
|
|
" use plugin Decho(https://github.com/vim-scripts/Decho) for debug
|
|
"
|
|
" turn off debug mode
|
|
" :%s;^\(\s*\)\(Decho\);\1"\2;g | :w | so %
|
|
"
|
|
" turn on debug mode
|
|
" :%s;^\(\s*\)"\(Decho\);\1\2;g | :w | so %
|
|
"
|
|
|
|
|
|
" vim:set foldmethod=marker:
|