import sublime import sublime_plugin import re import imp import json import sys import os.path import traceback BASE_PATH = os.path.abspath(os.path.dirname(__file__)) PACKAGES_PATH = sublime.packages_path() or os.path.dirname(BASE_PATH) # EMMET_GRAMMAR = os.path.join(BASE_PATH, 'Emmet.tmLanguage') EMMET_GRAMMAR = 'Packages/%s/Emmet.tmLanguage' % os.path.basename(BASE_PATH) sys.path += [BASE_PATH] + [os.path.join(BASE_PATH, f) for f in ['emmet_completions', 'emmet']] # Make sure all dependencies are reloaded on upgrade if 'emmet.reloader' in sys.modules: imp.reload(sys.modules['emmet.reloader']) import emmet.reloader # import completions as cmpl import emmet.pyv8loader as pyv8loader import emmet_completions as cmpl from emmet_completions.meta import HTML_ELEMENTS_ATTRIBUTES, HTML_ATTRIBUTES_VALUES from emmet.context import Context from emmet.context import js_file_reader as _js_file_reader from emmet.pyv8loader import LoaderDelegate __version__ = '1.1' __core_version__ = '1.0' __authors__ = ['"Sergey Chikuyonok" ' '"Nicholas Dudfield" '] is_python3 = sys.version_info[0] > 2 # JS context ctx = None # Emmet Settings settings = None # Default ST settings user_settings = None def is_st3(): return sublime.version()[0] == '3' def js_file_reader(file_path, use_unicode=True): if hasattr(sublime, 'load_resource'): rel_path = file_path for prefix in [sublime.packages_path(), sublime.installed_packages_path()]: if rel_path.startswith(prefix): rel_path = os.path.join('Packages', rel_path[len(prefix) + 1:]) break rel_path = rel_path.replace('.sublime-package', '') # for Windows we have to replace slashes rel_path = rel_path.replace('\\', '/') return sublime.load_resource(rel_path) return _js_file_reader(file_path, use_unicode) def init(): "Init Emmet plugin" # load settings globals()['user_settings'] = sublime.load_settings('Preferences.sublime-settings') globals()['settings'] = sublime.load_settings('Emmet.sublime-settings') settings.add_on_change('extensions_path', update_settings) # setup environment for PyV8 loading pyv8_paths = [ os.path.join(PACKAGES_PATH, 'PyV8'), os.path.join(PACKAGES_PATH, 'PyV8', pyv8loader.get_arch()), os.path.join(PACKAGES_PATH, 'PyV8', 'pyv8-%s' % pyv8loader.get_arch()) ] sys.path += pyv8_paths # unpack recently loaded binary, is exists for p in pyv8_paths: pyv8loader.unpack_pyv8(p) # provide some contributions to JS contrib = { 'sublime': sublime, 'sublimeReplaceSubstring': replace_substring, 'sublimeGetOption': settings.get } # create JS environment delegate = SublimeLoaderDelegate() globals()['ctx'] = Context( files=['../editor.js'], ext_path=settings.get('extensions_path', None), contrib=contrib, logger=delegate.log, reader=js_file_reader ) update_settings() pyv8loader.load(pyv8_paths[1], delegate) if settings.get('remove_html_completions', False): sublime.set_timeout(cmpl.remove_html_completions, 2000) class SublimeLoaderDelegate(LoaderDelegate): def __init__(self, settings=None): if settings is None: settings = {} for k in ['http_proxy', 'https_proxy', 'timeout']: if user_settings.has(k): settings[k] = user_settings.get(k, None) LoaderDelegate.__init__(self, settings) self.state = None self.message = 'Loading PyV8 binary, please wait' self.i = 0 self.addend = 1 self.size = 8 def on_start(self, *args, **kwargs): self.state = 'loading' def on_progress(self, *args, **kwargs): if kwargs['progress'].is_background: return before = self.i % self.size after = (self.size - 1) - before msg = '%s [%s=%s]' % (self.message, ' ' * before, ' ' * after) if not after: self.addend = -1 if not before: self.addend = 1 self.i += self.addend sublime.set_timeout(lambda: sublime.status_message(msg), 0) def on_complete(self, *args, **kwargs): self.state = 'complete' if kwargs['progress'].is_background: return sublime.set_timeout(lambda: sublime.status_message('PyV8 binary successfully loaded'), 0) def on_error(self, exit_code=-1, thread=None): self.state = 'error' sublime.set_timeout(lambda: show_pyv8_error(exit_code), 0) def setting(self, name, default=None): "Returns specified setting name" return self.settings.get(name, default) def log(self, message): print('Emmet: %s' % message) def show_pyv8_error(exit_code): if 'PyV8' not in sys.modules: sublime.error_message('Error while loading PyV8 binary: exit code %s \nTry to manually install PyV8 from\nhttps://github.com/emmetio/pyv8-binaries' % exit_code) def active_view(): return sublime.active_window().active_view() def check_context(verbose=False): "Checks if JS context is completely available" if not ctx.js(): if verbose: sublime.message_dialog('Please wait a bit while PyV8 binary is being downloaded') return False return True def replace_substring(start, end, value, no_indent=False): view = active_view() view.sel().clear() view.sel().add(sublime.Region(start, end or start)) if not is_python3: value = value.decode('utf-8') # XXX a bit naive indentation control. It handles most common # `no_indent` usages like replacing CSS rule content, but may not # produce expected result in all possible situations if no_indent: line = view.substr(view.line(view.sel()[0])) value = unindent_text(value, get_line_padding(line)) view.run_command('insert_snippet', {'contents': value}) def unindent_text(text, pad): """ Removes padding at the beginning of each text's line @type text: str @type pad: str """ lines = text.splitlines() for i,line in enumerate(lines): if line.startswith(pad): lines[i] = line[len(pad):] return '\n'.join(lines) def get_line_padding(line): """ Returns padding of current editor's line @return str """ m = re.match(r'^(\s+)', line) return m and m.group(0) or '' def update_settings(): ctx.set_ext_path(settings.get('extensions_path', None)) keys = ['snippets', 'preferences', 'syntaxProfiles', 'profiles'] payload = {} for k in keys: data = settings.get(k, None) if data: payload[k] = data ctx.reset() ctx.load_user_data(json.dumps(payload)) ctx.js() def get_scope(view, pt=-1): if pt == -1: # use current caret position pt = view.sel()[0].begin() if hasattr(view, 'scope_name'): return view.scope_name(pt) return view.syntax_name(pt) def should_perform_action(name, view=None): if not view: view = active_view() # fallback to old check if not view.settings().get('enable_emmet_keymap', True): return False disabled_actions = settings.get('disabled_keymap_actions', '') if not disabled_actions: # no disabled actions return True if disabled_actions == 'all': # disable all actions return False return name not in re.split(r'\s*,\s*', disabled_actions.strip()) def should_handle_tab_key(syntax=None): view = active_view() scopes = settings.get('disabled_single_snippet_for_scopes', None) cur_scope = get_scope(view) if sublime.score_selector(cur_scope, 'source.css'): return True if not scopes or not sublime.score_selector(cur_scope, scopes): return True abbr = ctx.js().locals.pyExtractAbbreviation() disabled_snippets = settings.get('disabled_single_snippets', '').split() if disabled_snippets and abbr in disabled_snippets: return False if not re.match(r'^[\w\:%]+$', abbr): # it's a complex expression return True if re.match(r'^(lorem|lipsum)([a-z]{2})?\d*$', abbr): # hardcoded Lorem Ipsum generator return True # detect inline CSS if syntax is None: syntax = ctx.js().locals.pyGetSyntax(); if syntax == 'css': return True known_tags = settings.get('known_html_tags', '').split() if abbr in known_tags or ctx.js().locals.pyHasSnippet(abbr): return True return False def log(message): if settings.get('debug', False): print('Emmet: %s' % message) class RunEmmetAction(sublime_plugin.TextCommand): def run(self, edit, action=None, **kw): run_action(lambda i, sel: ctx.js().locals.pyRunAction(action)) # ctx.js().locals.pyRunAction(action) class ActionContextHandler(sublime_plugin.EventListener): def on_query_context(self, view, key, op, operand, match_all): if not key.startswith('emmet_action_enabled.'): return None prefix, name = key.split('.') return should_perform_action(name, view) def get_edit(view, edit_token=None): edit = None try: edit = view.begin_edit() except: pass if not edit and edit_token: try: edit = view.begin_edit(edit_token, 'Emmet') except Exception as e: pass return edit def run_action(action, view=None): if not check_context(True): return "Runs Emmet action in multiselection mode" if not view: view = active_view() region_key = '__emmet__' sels = list(view.sel()) r = ctx.js().locals.pyRunAction result = False # edit = get_edit(view, edit_token) max_sel_ix = len(sels) - 1 try: for i, sel in enumerate(reversed(sels)): view.sel().clear() view.sel().add(sel) # run action # result = r(name) or result result = action(max_sel_ix - i, sel) or result # remember resulting selections view.add_regions(region_key, (view.get_regions(region_key) + list(view.sel())) , '') except Exception as e: view.erase_regions(region_key) print(traceback.format_exc()) return # output all saved regions as selection view.sel().clear() for sel in view.get_regions(region_key): view.sel().add(sel) view.erase_regions(region_key) # if edit: # view.end_edit(edit) return result class TabAndCompletionsHandler(): def correct_syntax(self, view, syntax='html'): return syntax == 'html' and view.match_selector( view.sel()[0].b, cmpl.EMMET_SCOPE ) def completion_handler(self, view): "Returns completions handler fo current caret position" black_list = settings.get('completions_blacklist', []) # A mapping of scopes, sub scopes and handlers, first matching of which # is used. COMPLETIONS = ( (cmpl.HTML_INSIDE_TAG, self.html_elements_attributes), (cmpl.HTML_INSIDE_TAG_ATTRIBUTE, self.html_attributes_values) ) pos = view.sel()[0].b # Try to find some more specific contextual abbreviation for sub_selector, handler in COMPLETIONS: h_name = handler.__name__ if not black_list or h_name in black_list: continue if (view.match_selector(pos, sub_selector) or view.match_selector(pos - 1, sub_selector)): return handler return None def html_elements_attributes(self, view, prefix, pos): tag = cmpl.find_tag_name(view, pos) values = HTML_ELEMENTS_ATTRIBUTES.get(tag, []) return [(v, '%s\t@%s' % (v,v), '%s="$1"' % v) for v in values] def html_attributes_values(self, view, prefix, pos): attr = cmpl.find_attribute_name(view, pos) values = HTML_ATTRIBUTES_VALUES.get(attr, []) return [(v, '%s\t@=%s' % (v,v), v) for v in values] def expand_by_tab(self, view): if not check_context(): return False; syntax = ctx.js().locals.pyGetSyntax(); if not should_handle_tab_key(syntax): return False # we need to filter out attribute completions if # 'disable_completions' option is not active if (not settings.get('disable_completions', False) and self.correct_syntax(view, syntax) and self.completion_handler(view)): return None caret_pos = view.sel()[0].begin() cur_scope = get_scope(view) # let's see if Tab key expander should be disabled for current scope banned_scopes = settings.get('disable_tab_abbreviations_for_scopes', '') if banned_scopes and view.score_selector(caret_pos, banned_scopes): return None # Sometimes ST2 matcher may incorrectly filter scope context, # check it against special regexp banned_regexp = settings.get('disable_tab_abbreviations_for_regexp', None) if banned_regexp and re.search(banned_regexp, cur_scope): return None return run_action(lambda i, sel: ctx.js().locals.pyRunAction('expand_abbreviation')) # view.run_command('run_emmet_action', # {'action':'expand_abbreviation'}) class ExpandAbbreviationByTab(sublime_plugin.TextCommand): def run(self, edit, **kw): if settings.get('use_old_tab_handler', False): return view = active_view() h = TabAndCompletionsHandler() if not h.expand_by_tab(view): # try to mimic default Tab behaviour of Sublime Text view.run_command('insert_best_completion', { 'default': '\t', 'exact': user_settings.get('tab_completion', True) }) class TabExpandHandler(sublime_plugin.EventListener): def on_query_context(self, view, key, op, operand, match_all): if key != 'is_abbreviation': return None if settings.get('use_old_tab_handler', False): h = TabAndCompletionsHandler() return h.expand_by_tab(view) return check_context() def on_query_completions(self, view, prefix, locations): h = TabAndCompletionsHandler() if view.match_selector(locations[0], settings.get('css_completions_scope', '')) and check_context(): l = [] if settings.get('show_css_completions', False): completions = ctx.js().locals.pyGetCSSCompletions() if completions: for p in completions: l.append(('%s\t%s' % (p['k'], p['label']), p['v'])) if not l: return [] return (l, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS) if not h.correct_syntax(view) or settings.get('disable_completions', False): return [] handler = h.completion_handler(view) if handler: pos = view.sel()[0].b completions = handler(view, prefix, pos) return completions return [] class CommandsAsYouTypeBase(sublime_plugin.TextCommand): input_message = "Enter Input" default_input = "" process_panel_input = lambda s, i: i.title() # Note that this must be of form `Packages/$Package/Emmet.tmLanguage` on ST3 # NOT an absolute path! panel_grammar = EMMET_GRAMMAR def is_enabled(self): return True def run_command(self, edit, view, processed_input): if '\n' in processed_input: for sel in view.sel(): trailing = sublime.Region(sel.end(), view.line(sel).end()) if view.substr(trailing).isspace(): view.erase(edit, trailing) if not is_python3: processed_input = processed_input.decode('utf-8') view.run_command('insert_snippet', { 'contents': processed_input }) def on_panel_change(self, abbr): if not abbr and self.erase: self.undo() self.erase = False return def inner_insert(): self.view.run_command(self.name(), dict(panel_input=abbr)) # self.view.run_command('hide_auto_complete') self.undo() sublime.set_timeout(inner_insert, 0) def undo(self): if self.erase: sublime.set_timeout(lambda: self.view.run_command('undo'), 0) def remember_sels(self, view): self._sels = list(view.sel()) self._sel_items = [] for sel in self._sels: # selection should be unindented in order to get desired result line = view.substr(view.line(sel)) s = view.substr(sel) self._sel_items.append(unindent_text(s, get_line_padding(line))) def on_panel_done(self, abbr): pass def run(self, edit, panel_input=None, **kwargs): if panel_input is None: self.setup(edit, self.view, **kwargs) self.erase = False panel = self.view.window().show_input_panel ( self.input_message, self.default_input, self.on_panel_done, # on_done self.on_panel_change, # on_change self.undo) # on_cancel panel.sel().clear() panel.sel().add(sublime.Region(0, panel.size())) if self.panel_grammar: panel.set_syntax_file(self.panel_grammar) panel_setting = panel.settings().set panel_setting('line_numbers', False) panel_setting('gutter', False) panel_setting('auto_complete', False) panel_setting('tab_completion', False) else: self.run_on_input(edit, self.view, panel_input) def setup(self, edit, view, **kwargs): pass def run_on_input(self, edit, view, panel_input): view = self.view cmd_input = self.process_panel_input(panel_input) or '' try: self.erase = self.run_command(edit, view, cmd_input) is not False except: pass class WrapAsYouType(CommandsAsYouTypeBase): default_input = 'div' _prev_output = '' input_message = "Enter Wrap Abbreviation: " def setup(self, edit, view, **kwargs): self._prev_output = '' if len(view.sel()) == 1: # capture wrapping context (parent HTML element) # if there is only one selection r = ctx.js().locals.pyCaptureWrappingRange() if r: view.sel().clear() view.sel().add(sublime.Region(r[0], r[1])) view.show(view.sel()) self.remember_sels(view) # override method to correctly wrap abbreviations def run_on_input(self, edit, view, abbr): # def _real_insert(self, abbr): # view = self.view # self.edit = get_edit(view, self.edit_token) self.erase = True # restore selections view.sel().clear() for sel in self._sels: view.sel().add(sel) def ins(i, sel): try: self._prev_output = ctx.js().locals.pyWrapAsYouType(abbr, self._sel_items[i]) # self.run_command(view, output) except Exception: "dont litter the console" self.run_command(edit, view, self._prev_output) run_action(ins, view) # if self.edit: # view.end_edit(self.edit) class ExpandAsYouType(WrapAsYouType): default_input = 'div' input_message = "Enter Abbreviation: " def setup(self, edit, view, **kwargs): # adjust selection to non-space bounds sels = [] for s in view.sel(): text = view.substr(s) a = s.a + len(text) - len(text.lstrip()) b = s.b - len(text) + len(text.rstrip()) sels.append(sublime.Region(a, b)) view.sel().clear() for s in sels: view.sel().add(s) self.remember_sels(active_view()) class EnterKeyHandler(sublime_plugin.EventListener): def on_query_context(self, view, key, op, operand, match_all): if key != 'clear_fields_on_enter_key': return None if settings.get('clear_fields_on_enter_key', False): view.run_command('clear_fields') return True class RenameTag(sublime_plugin.TextCommand): def run(self, edit, **kw): if not check_context(True): return view = active_view() sels = list(view.sel()) sel_cleared = False for s in sels: ranges = ctx.js().locals.pyGetTagNameRanges(s.begin()) if ranges: if not sel_cleared: view.sel().clear() sel_cleared = True for r in ranges: view.sel().add(sublime.Region(r[0], r[1])) view.show(view.sel()) class EmmetInsertAttribute(sublime_plugin.TextCommand): def run(self, edit, attribute=None, **kw): if not attribute: return view = active_view() prefix = '' if view.sel(): sel = view.sel()[0] if not view.substr(sublime.Region(sel.begin() - 1, sel.begin())).isspace(): prefix = ' ' view.run_command('insert_snippet', {'contents': '%s%s="$1"' % (prefix, attribute)}) class EmmetResetContext(sublime_plugin.TextCommand): def run(self, edit, **kw): update_settings() def plugin_loaded(): sublime.set_timeout(init, 200) ################## # Init plugin if not is_python3: init()