#!/usr/bin/env python # encoding: utf-8 """Code to provide access to UltiSnips files from disk.""" from collections import defaultdict import hashlib import os from UltiSnips import _vim from UltiSnips import compatibility from UltiSnips.snippet.source._base import SnippetSource def _hash_file(path): """Returns a hashdigest of 'path'.""" if not os.path.isfile(path): return False return hashlib.sha1(open(path, 'rb').read()).hexdigest() class SnippetSyntaxError(RuntimeError): """Thrown when a syntax error is found in a file.""" def __init__(self, filename, line_index, msg): RuntimeError.__init__(self, '%s in %s:%d' % ( msg, filename, line_index)) class SnippetFileSource(SnippetSource): """Base class that abstracts away 'extends' info and file hashes.""" def __init__(self): SnippetSource.__init__(self) self._files_for_ft = defaultdict(set) self._file_hashes = defaultdict(lambda: None) self._ensure_cached = False def ensure(self, filetypes, cached): if cached and self._ensure_cached: return for ft in self.get_deep_extends(filetypes): if self._needs_update(ft): self._load_snippets_for(ft) self._ensure_cached = True def _get_all_snippet_files_for(self, ft): """Returns a set of all files that define snippets for 'ft'.""" raise NotImplementedError() def _parse_snippet_file(self, filedata, filename): """Parses 'filedata' as a snippet file and yields events.""" raise NotImplementedError() def _needs_update(self, ft): """Returns true if any files for 'ft' have changed and must be reloaded.""" existing_files = self._get_all_snippet_files_for(ft) if existing_files != self._files_for_ft[ft]: self._files_for_ft[ft] = existing_files return True for filename in self._files_for_ft[ft]: if _hash_file(filename) != self._file_hashes[filename]: return True return False def _load_snippets_for(self, ft): """Load all snippets for the given 'ft'.""" if ft in self._snippets: del self._snippets[ft] del self._extends[ft] try: for fn in self._files_for_ft[ft]: self._parse_snippets(ft, fn) except: del self._files_for_ft[ft] raise # Now load for the parents for parent_ft in self.get_deep_extends([ft]): if parent_ft != ft and self._needs_update(parent_ft): self._load_snippets_for(parent_ft) def _parse_snippets(self, ft, filename): """Parse the 'filename' for the given 'ft' and watch it for changes in the future.""" self._file_hashes[filename] = _hash_file(filename) file_data = compatibility.open_ascii_file(filename, 'r').read() for event, data in self._parse_snippet_file(file_data, filename): if event == 'error': msg, line_index = data filename = _vim.eval("""fnamemodify(%s, ":~:.")""" % _vim.escape(filename)) raise SnippetSyntaxError(filename, line_index, msg) elif event == 'clearsnippets': priority, triggers = data self._snippets[ft].clear_snippets(priority, triggers) elif event == 'extends': # TODO(sirver): extends information is more global # than one snippet source. filetypes, = data self.update_extends(ft, filetypes) elif event == 'snippet': snippet, = data self._snippets[ft].add_snippet(snippet) else: assert False, 'Unhandled %s: %r' % (event, data)