504 lines
16 KiB
VimL
504 lines
16 KiB
VimL
" Vim completion script
|
|
" Language: htmldjango
|
|
" Maintainer: Michael Brown
|
|
" Last Change: Sun 13 May 2012 16:39:45 EST
|
|
" Version: 0.9.2
|
|
" Omnicomplete for django template taga/variables/filters
|
|
" {{{1 Environment Settings
|
|
if !exists('g:htmldjangocomplete_html_flavour')
|
|
" :verbose function htmlcomplete#CheckDoctype for details
|
|
" No html5!
|
|
"'html401t' 'xhtml10s' 'html32' 'html40t' 'html40f' 'html40s'
|
|
"'html401t' 'html401f' 'html401s' 'xhtml10t' 'xhtml10f' 'xhtml10s'
|
|
"'xhtml11'
|
|
let g:htmldjangocomplete_html_flavour = 'xhtml11'
|
|
endif
|
|
|
|
"Allow settings of DEBUG
|
|
if !exists('g:htmldjangocomplete_debug')
|
|
let g:htmldjangocomplete_debug = 0
|
|
endif
|
|
|
|
"{{{1 The actual omnifunc
|
|
function! htmldjangocomplete#CompleteDjango(findstart, base)
|
|
"{{{2 findstart = 1 when we need to get the text length
|
|
"
|
|
if a:findstart == 1
|
|
|
|
"Fallback to htmlcomplete
|
|
if searchpair('{{','','}}','nc') == 0 && searchpair('{%',"",'%}','nc') == 0
|
|
if !exists('b:html_doctype')
|
|
let b:html_doctype = 1
|
|
let b:html_omni_flavor = g:htmldjangocomplete_html_flavour
|
|
endif
|
|
return htmlcomplete#CompleteTags(a:findstart,a:base)
|
|
endif
|
|
|
|
" locate the start of the word
|
|
let line = getline('.')
|
|
let start = col('.') - 1
|
|
"special case for {% extends %} {% import %} needs to grab /'s
|
|
"TODO make this match more flexible. It needs to know its in a string
|
|
"also need to handle inline imports
|
|
if s:get_context() == 'extends'|| s:get_context() == 'include'
|
|
while start > 0 && line[start - 1] != '"' && line[start -1] != "'"
|
|
\ && line[start -1] != ' '
|
|
let start -= 1
|
|
endwhile
|
|
return start
|
|
endif
|
|
"
|
|
"default <word> case
|
|
while start > 0 && line[start - 1] =~ '\a'
|
|
let start -= 1
|
|
endwhile
|
|
return start
|
|
"{{{2 findstart = 0 when we need to return the list of completions
|
|
else
|
|
"Fallback to htmlcomplete
|
|
if searchpair('{{','','}}','nc') == 0 && searchpair('{%',"",'%}','nc') == 0
|
|
let matches = htmlcomplete#CompleteTags(a:findstart,a:base)
|
|
"suppress all DOCTYPE matches
|
|
call filter(matches, 'stridx(v:val["word"],"DOCTYPE") == -1')
|
|
return matches
|
|
endif
|
|
|
|
let context = s:get_context()
|
|
|
|
if context == 'extends' || context == 'include'
|
|
let context = 'template'
|
|
endif
|
|
|
|
"TODO: Reduce load always nature of this plugin
|
|
call s:load_libs()
|
|
"get context look for {% {{ and |
|
|
let line = getline('.')
|
|
let start = col('.') -1
|
|
|
|
" Special case for extends and import
|
|
" TODO 'filter' should really just be string filters
|
|
if index(['template','load','url','filter','block', 'static'],context) != -1
|
|
execute "python htmldjangocomplete('" . context . "', '" . a:base . "')"
|
|
return g:htmldjangocomplete_completions
|
|
endif
|
|
|
|
while start > 0
|
|
if line[start] == ':' && s:in_django(line,start) == 1
|
|
execute "python htmldjangocomplete('variable', '" . a:base . "')"
|
|
return g:htmldjangocomplete_completions
|
|
elseif line[start] == '|' && s:in_django(line,start)
|
|
execute "python htmldjangocomplete('filter', '" . a:base . "')"
|
|
return g:htmldjangocomplete_completions
|
|
elseif line[start] == '{' && line[start -1] == '{'
|
|
execute "python htmldjangocomplete('variable', '" . a:base . "')"
|
|
return g:htmldjangocomplete_completions
|
|
elseif line[start] == '%' && line[start -1] == '{'
|
|
execute "python htmldjangocomplete('tag', '" . a:base . "')"
|
|
return g:htmldjangocomplete_completions
|
|
else
|
|
let start -= 1
|
|
endif
|
|
endwhile
|
|
|
|
return [ {'word': "nomatch"} ]
|
|
"fallback to htmlcomplete TODO This doesn't work as expected.
|
|
"Might need to turn off some doctype setting.
|
|
"
|
|
"call htmlcomplete#CompleteTags(1, a:base)
|
|
"return htmlcomplete#CompleteTags(0, a:base)
|
|
endif
|
|
endfunction
|
|
|
|
"Supporting vim functions {{{1
|
|
function! s:get_context()
|
|
let curpos = getpos('.')
|
|
let line = getline('.')
|
|
|
|
"tags
|
|
let starttag = searchpairpos('{%', '', '%}', 'bn')
|
|
if starttag != [0,0]
|
|
let fragment = line[starttag[1]:curpos[2]]
|
|
return split(fragment,' ')[1]
|
|
endif
|
|
|
|
return "other"
|
|
endfunction
|
|
|
|
"TODO This could probably be neater with an index check. need to get strings
|
|
"working
|
|
function! s:in_django(l,s)
|
|
let line = a:l
|
|
let start = a:s
|
|
while start >= 0
|
|
if line[start] == '}'
|
|
return 0
|
|
elseif line[start] == '{'
|
|
return 1
|
|
endif
|
|
let start -= 1
|
|
endwhile
|
|
return 0
|
|
endfunction
|
|
|
|
"Python section {{{1
|
|
"imports {{{2
|
|
let g:htmldjangocomplete_completions = []
|
|
function! s:load_libs()
|
|
if has('python')
|
|
python << EOF
|
|
try:
|
|
HTMLDJANGO_DEBUG = vim.eval("g:htmldjangocomplete_debug") == 0 and True or False
|
|
except:
|
|
HTMLDJANGO_DEBUG = False
|
|
|
|
TEMPLATE_EXTS = ['.html','.txt','.htm']
|
|
|
|
import warnings
|
|
warnings.sys.warnoptions = ['-W']
|
|
|
|
import logging
|
|
import vim
|
|
import os
|
|
|
|
# Install the django-configurations importer (before Django setup).
|
|
if os.environ.get('DJANGO_CONFIGURATION'):
|
|
try:
|
|
import configurations.importer
|
|
configurations.importer.install()
|
|
except:
|
|
sys.exit()
|
|
|
|
# Setup Django (required for >= 1.7).
|
|
import django
|
|
if hasattr(django, 'setup'):
|
|
django.setup()
|
|
|
|
try:
|
|
#for OLD versions od django
|
|
from django.template import get_library
|
|
except:
|
|
#for django 1.8+
|
|
try:
|
|
from django.template.base import get_library
|
|
except:
|
|
#for django 1.9+
|
|
from django.template.backends.django import get_installed_libraries
|
|
def get_library(libname):
|
|
return get_installed_libraries()[libname]
|
|
|
|
|
|
from django.template.loaders import filesystem, app_directories
|
|
#Later versions of django seem to be fussy about get_library paths.
|
|
try:
|
|
from django.template import import_library
|
|
except ImportError:
|
|
try:
|
|
from django.template.base import import_library
|
|
except ImportError:
|
|
import_library = get_library
|
|
|
|
|
|
try:
|
|
from django.template.loaders.app_directories import app_template_dirs
|
|
except:
|
|
from django.template.loaders.app_directories import get_app_template_dirs
|
|
app_template_dirs = get_app_template_dirs('templates')
|
|
|
|
from django.conf import settings as mysettings
|
|
from django.template.loader import get_template
|
|
from django.template.loader_tags import ExtendsNode, BlockNode
|
|
from django.template import Template
|
|
|
|
import re
|
|
from operator import itemgetter
|
|
import pkgutil
|
|
import os
|
|
from glob import glob
|
|
|
|
try:
|
|
from django.template import get_templatetags_modules
|
|
except ImportError:
|
|
#I've lifted this version from the django source
|
|
try:
|
|
from importlib import import_module
|
|
except ImportError:
|
|
from django.utils.importlib import import_module
|
|
|
|
def get_templatetags_modules():
|
|
"""
|
|
Return the list of all available template tag modules.
|
|
|
|
Caches the result for faster access.
|
|
"""
|
|
_templatetags_modules = []
|
|
# Populate list once per process. Mutate the local list first, and
|
|
# then assign it to the global name to ensure there are no cases where
|
|
# two threads try to populate it simultaneously.
|
|
for app_module in ['django'] + list(mysettings.INSTALLED_APPS):
|
|
try:
|
|
templatetag_module = '%s.templatetags' % app_module
|
|
import_module(templatetag_module)
|
|
_templatetags_modules.append(templatetag_module)
|
|
except ImportError:
|
|
continue
|
|
return _templatetags_modules
|
|
|
|
# {{{2 Support functions
|
|
|
|
def get_block_tags(start=''):
|
|
|
|
#use regexp for extends as get_template will fail on tag errors
|
|
rexp = re.compile('{%\s*extends\s*[\'"](.*)["\']\s*%}')
|
|
base = None
|
|
templates = [] # for cycle detection
|
|
|
|
for l in vim.current.buffer[0:10]:
|
|
match = rexp.match(l)
|
|
if match:
|
|
try:
|
|
base = get_template(match.groups()[0])
|
|
except:
|
|
return []
|
|
|
|
if not base:
|
|
return []
|
|
|
|
def _get_blocks(tpl, menu_prefix='', add_name = True):
|
|
"""
|
|
recursive worker function
|
|
"""
|
|
|
|
# TODO I Think this is a 1.8 compatibility thing sending the wrapper
|
|
# class
|
|
tpl = getattr(tpl, 'name', None) and tpl or tpl.template
|
|
|
|
if tpl.name in templates and isinstance(tpl, Template):
|
|
logging.info("cyclic extends detected!")
|
|
return []
|
|
else:
|
|
templates.append(tpl.name)
|
|
|
|
if menu_prefix == '' and isinstance(tpl, Template):
|
|
menu_prefix = "%s > " % tpl.name
|
|
|
|
blocks = [(block, block.name, menu_prefix)
|
|
for block in tpl.nodelist if isinstance(block, BlockNode)]
|
|
|
|
for block, name, _ in blocks:
|
|
if add_name:
|
|
blocks += _get_blocks(block, '%s%s > ' % (menu_prefix, name))
|
|
else:
|
|
blocks += _get_blocks(block, '%s' % (menu_prefix))
|
|
|
|
if len(tpl.nodelist) > 0 and isinstance(tpl.nodelist[0], ExtendsNode):
|
|
logging.debug("parent_name")
|
|
logging.debug(tpl.nodelist[0].parent_name)
|
|
parent_template = str(tpl.nodelist[0].parent_name).replace(
|
|
'"', '').replace("''", '')
|
|
try:
|
|
parent_tpl = get_template(parent_template)
|
|
blocks += _get_blocks(parent_tpl)
|
|
except TemplateDoesNotExist:
|
|
logging.info("get_template:TemplateDoesNotExist - '%s'" %
|
|
parent_template)
|
|
|
|
for node in tpl.nodelist:
|
|
if not isinstance(node, BlockNode) and not isinstance(node,
|
|
ExtendsNode):
|
|
try:
|
|
blocks += _get_blocks(node, menu_prefix, add_name=False)
|
|
except AttributeError:
|
|
logging.info("node %s: no nodelist" % node)
|
|
|
|
return blocks
|
|
|
|
full_matches = _get_blocks(base)
|
|
|
|
names = []
|
|
matches = []
|
|
# dedup matches TODO might be a better way of picking matches
|
|
for match in full_matches:
|
|
if not match[0] in names:
|
|
matches.append(match)
|
|
names.append(match[0])
|
|
|
|
return [{'word':n,'menu':m} for b, n, m in matches if n.startswith(start)]
|
|
|
|
def get_template_names(pattern):
|
|
|
|
dirs = getattr(mysettings, "TEMPLATE_DIRS", ()) + app_template_dirs
|
|
if hasattr(mysettings, "TEMPLATES"):
|
|
for d in [e["DIRS"] for e in mysettings.TEMPLATES]:
|
|
dirs += tuple(d)
|
|
|
|
matches = []
|
|
for d in dirs:
|
|
d = d + (os.path.sep if not d.endswith(os.path.sep) else '')
|
|
for m in glob(os.path.join(d,pattern + '*')):
|
|
if os.path.isdir(m):
|
|
for root,dirnames,filenames in os.walk(m):
|
|
for f in filenames:
|
|
fn,ext = os.path.splitext(f)
|
|
if ext in TEMPLATE_EXTS:
|
|
matches.append({
|
|
'word' : os.path.join(root,f).replace(d,''),
|
|
'info' : 'found in %s' % d
|
|
}
|
|
)
|
|
else:
|
|
matches.append({
|
|
'word' : m.replace(d,''),
|
|
'info' : 'found in %s' % d
|
|
}
|
|
)
|
|
|
|
return matches
|
|
|
|
try:
|
|
from django.contrib.staticfiles import finders, storage
|
|
def get_staticfiles(pattern):
|
|
|
|
dirs = mysettings.STATICFILES_DIRS
|
|
|
|
#TODO crude matching
|
|
line = vim.current.line
|
|
if 'script' in line:
|
|
ext = ".*\.js$"
|
|
elif 'style' in line:
|
|
ext = ".*\.css$"
|
|
elif 'img' in line:
|
|
ext = ".*\.(gif|jpg|jpeg|png)$"
|
|
else:
|
|
ext = '.*'
|
|
|
|
matches = []
|
|
|
|
for finder in finders.get_finders():
|
|
for path, storage in finder.list([]):
|
|
if re.compile(ext,re.IGNORECASE).match(path) \
|
|
and path.startswith(pattern):
|
|
matches.append(dict(word=path,info=''))
|
|
|
|
return matches
|
|
except:
|
|
def get_staticfiles(pattern):
|
|
return []
|
|
|
|
def get_tag_libraries():
|
|
opts = []
|
|
for module in get_templatetags_modules():
|
|
mod = __import__(module,fromlist=['foo'])
|
|
for l,m,i in pkgutil.iter_modules([os.path.dirname(mod.__file__)]):
|
|
opts.append({'word':m,'menu':mod.__name__})
|
|
|
|
return opts
|
|
|
|
|
|
def _get_doc(doc, name):
|
|
if doc:
|
|
return doc.replace('"',' ').replace("'",' ')
|
|
return '%s: no doc' % name
|
|
|
|
def _get_opt_dict(lib,t,libname=''):
|
|
opts = getattr(lib,t)
|
|
return [
|
|
{'word':f, 'info': _get_doc(opts[f].__doc__,f),'menu':libname} \
|
|
for f in opts.keys()]
|
|
|
|
def load_app_tags():
|
|
cb = vim.current.buffer
|
|
for line in cb:
|
|
m = re.compile('{% load (.*)%}').match(line)
|
|
if m:
|
|
for lib in m.groups()[0].rstrip().split(' '):
|
|
try:
|
|
l = get_library(lib)
|
|
htmldjango_opts['filter'] += _get_opt_dict(l,'filters',lib)
|
|
htmldjango_opts['tag'] += _get_opt_dict(l,'tags',lib)
|
|
except Exception as e:
|
|
if HTMLDJANGO_DEBUG:
|
|
print "FAILED TO LOAD: %s" % lib
|
|
raise e
|
|
# {{{2 load options
|
|
# TODO At the moment this is being loaded every match
|
|
htmldjango_opts = {}
|
|
|
|
htmldjango_opts['load'] = get_tag_libraries()
|
|
def_filters = import_library('django.template.defaultfilters')
|
|
htmldjango_opts['filter'] = _get_opt_dict(def_filters,'filters','default')
|
|
def_tags = import_library('django.template.defaulttags')
|
|
htmldjango_opts['tag'] = _get_opt_dict(def_tags,'tags','default')
|
|
load_app_tags()
|
|
|
|
try:
|
|
urls = __import__(mysettings.ROOT_URLCONF,fromlist=['foo'])
|
|
except:
|
|
urls = None
|
|
|
|
def htmldjango_urls(pattern):
|
|
matches = []
|
|
def get_urls(urllist,parent=None):
|
|
for entry in urllist:
|
|
if hasattr(entry,'name') and entry.name:
|
|
matches.append(dict(
|
|
word = entry.name,
|
|
info = entry.regex.pattern,
|
|
menu = parent and parent.urlconf_name or '')
|
|
)
|
|
if hasattr(entry, 'url_patterns'):
|
|
get_urls(entry.url_patterns, entry)
|
|
if urls:
|
|
get_urls(urls.urlpatterns)
|
|
return matches
|
|
|
|
#TODO I may be able to populate RequestContext via middleware component
|
|
htmldjango_opts['variable'] = []
|
|
#TODO Write a function that gets all ancestor template blocks
|
|
htmldjango_opts['block'] = []
|
|
|
|
# Main Python function {{{2
|
|
def htmldjangocomplete(context,match):
|
|
if context == 'template':
|
|
all = get_template_names(match)
|
|
elif context == 'static':
|
|
all = get_staticfiles(match)
|
|
elif context == 'url':
|
|
all = htmldjango_urls(match)
|
|
elif context == 'block':
|
|
all = get_block_tags(match)
|
|
else:
|
|
all = htmldjango_opts[context]
|
|
|
|
vim.command("silent let g:htmldjangocomplete_completions = []")
|
|
|
|
dictstr = '['
|
|
# have to do this for double quoting
|
|
|
|
all = [a for a in all if a['word'].startswith(match)]
|
|
all = sorted(all, key=itemgetter('word'))
|
|
|
|
for cmpl in all:
|
|
dictstr += '{'
|
|
for x in cmpl: dictstr += '"%s":"%s",' % (x,cmpl[x].replace("\\", "\\\\")) # Windows slashes must be escaped
|
|
dictstr += '"icase":0},'
|
|
if dictstr[-1] == ',': dictstr = dictstr[:-1]
|
|
dictstr += ']'
|
|
vim.command("silent let g:htmldjangocomplete_completions = %s" % dictstr)
|
|
EOF
|
|
endif
|
|
endfunction
|
|
" Test Area {{{1
|
|
function! TestLoadLibs()
|
|
call s:load_libs()
|
|
endfunction
|
|
|
|
function! HtmlDjangoDebug(on)
|
|
if a:on
|
|
echo "adding Breakpoint"
|
|
breakadd func htmldjangocomplete#CompleteDjango
|
|
else
|
|
echo "remove Breakpoint"
|
|
breakdel func htmldjangocomplete#CompleteDjango
|
|
endif
|
|
endfunction
|
|
" vim:set foldmethod=marker:
|