def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_mappings_in_place = False self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source('ultisnips_files', UltiSnipsFileSource()) self.register_snippet_source('added', self._added_snippets_source) enable_snipmate = True if _vim.eval("exists('g:UltiSnipsEnableSnipMate')") == '1': enable_snipmate = _vim.eval('g:UltiSnipsEnableSnipMate') if enable_snipmate == '1': self.register_snippet_source('snipmate_files', SnipMateFileSource()) self._reinit()
def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_state_up = False self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._snip_expanded_in_action = False self._inside_action = False self._last_inserted_char = '' self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source('ultisnips_files', UltiSnipsFileSource()) self.register_snippet_source('added', self._added_snippets_source) enable_snipmate = '1' if _vim.eval("exists('g:UltiSnipsEnableSnipMate')") == '1': enable_snipmate = _vim.eval('g:UltiSnipsEnableSnipMate') if enable_snipmate == '1': self.register_snippet_source('snipmate_files', SnipMateFileSource()) self._should_update_textobjects = False self._should_reset_visual = False self._reinit()
def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_state_up = False self._supertab_keys = None self._active_snippets = [] self._added_buffer_filetypes = defaultdict(lambda: []) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._snip_expanded_in_action = False self._inside_action = False self._last_change = ("", 0) self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source("ultisnips_files", UltiSnipsFileSource()) self.register_snippet_source("added", self._added_snippets_source) enable_snipmate = "1" if vim_helper.eval("exists('g:UltiSnipsEnableSnipMate')") == "1": enable_snipmate = vim_helper.eval("g:UltiSnipsEnableSnipMate") if enable_snipmate == "1": self.register_snippet_source("snipmate_files", SnipMateFileSource()) self._should_update_textobjects = False self._should_reset_visual = False self._reinit()
def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_state_up = False self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ["all"]) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._snip_expanded_in_action = False self._inside_action = False self._last_inserted_char = "" self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source("ultisnips_files", UltiSnipsFileSource()) self.register_snippet_source("added", self._added_snippets_source) enable_snipmate = "1" if _vim.eval("exists('g:UltiSnipsEnableSnipMate')") == "1": enable_snipmate = _vim.eval("g:UltiSnipsEnableSnipMate") if enable_snipmate == "1": self.register_snippet_source("snipmate_files", SnipMateFileSource()) self._reinit()
def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_mappings_in_place = False self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source("ultisnips_files", UltiSnipsFileSource()) self.register_snippet_source("added", self._added_snippets_source) if _vim.eval("exists('g:UltiSnipsDisableSnipMate')") == "1": disable_snipmate = _vim.eval("g:UltiSnipsDisableSnipMate") else: disable_snipmate = "0" if disable_snipmate != "1": self.register_snippet_source("snipmate_files", SnipMateFileSource()) self._reinit()
def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._added_snippets_source = AddedSnippetsSource() self._snippet_sources = [ UltiSnipsFileSource(), self._added_snippets_source, SnipMateFileSource(), ] self._reinit()
def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_mappings_in_place = False self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source("ultisnips_files", UltiSnipsFileSource()) self.register_snippet_source("added", self._added_snippets_source) self.register_snippet_source("snipmate_files", SnipMateFileSource()) self._reinit()
def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source("ultisnips_files", UltiSnipsFileSource()) self.register_snippet_source("added", self._added_snippets_source) self.register_snippet_source("snipmate_files", SnipMateFileSource()) self._reinit()
class SnippetManager(object): """The main entry point for all UltiSnips functionality. All Vim functions call methods in this class. """ def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_state_up = False self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._snip_expanded_in_action = False self._inside_action = False self._last_inserted_char = '' self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source('ultisnips_files', UltiSnipsFileSource()) self.register_snippet_source('added', self._added_snippets_source) enable_snipmate = '1' if _vim.eval("exists('g:UltiSnipsEnableSnipMate')") == '1': enable_snipmate = _vim.eval('g:UltiSnipsEnableSnipMate') if enable_snipmate == '1': self.register_snippet_source('snipmate_files', SnipMateFileSource()) self._should_update_textobjects = False self._should_reset_visual = False self._reinit() @err_to_scratch_buffer def jump_forwards(self): """Jumps to the next tabstop.""" _vim.command('let g:ulti_jump_forwards_res = 1') _vim.command('let &undolevels = &undolevels') if not self._jump(): _vim.command('let g:ulti_jump_forwards_res = 0') return self._handle_failure(self.forward_trigger) @err_to_scratch_buffer def jump_backwards(self): """Jumps to the previous tabstop.""" _vim.command('let g:ulti_jump_backwards_res = 1') _vim.command('let &undolevels = &undolevels') if not self._jump(True): _vim.command('let g:ulti_jump_backwards_res = 0') return self._handle_failure(self.backward_trigger) @err_to_scratch_buffer def expand(self): """Try to expand a snippet at the current position.""" _vim.command('let g:ulti_expand_res = 1') if not self._try_expand(): _vim.command('let g:ulti_expand_res = 0') self._handle_failure(self.expand_trigger) @err_to_scratch_buffer def expand_or_jump(self): """This function is used for people who wants to have the same trigger for expansion and forward jumping. It first tries to expand a snippet, if this fails, it tries to jump forward. """ _vim.command('let g:ulti_expand_or_jump_res = 1') rv = self._try_expand() if not rv: _vim.command('let g:ulti_expand_or_jump_res = 2') rv = self._jump() if not rv: _vim.command('let g:ulti_expand_or_jump_res = 0') self._handle_failure(self.expand_trigger) @err_to_scratch_buffer def snippets_in_current_scope(self, searchAll): """Returns the snippets that could be expanded to Vim as a global variable.""" before = '' if searchAll else _vim.buf.line_till_cursor snippets = self._snips(before, True) # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) for snip in snippets: description = snip.description[snip.description.find(snip.trigger) + len(snip.trigger) + 2:] location = snip.location if snip.location else '' key = as_unicode(snip.trigger) description = as_unicode(description) # remove surrounding "" or '' in snippet description if it exists if len(description) > 2: if (description[0] == description[-1] and description[0] in "'\""): description = description[1:-1] _vim.command(as_unicode( "let g:current_ulti_dict['{key}'] = '{val}'").format( key=key.replace("'", "''"), val=description.replace("'", "''"))) if searchAll: _vim.command(as_unicode( ("let g:current_ulti_dict_info['{key}'] = {{" "'description': '{description}'," "'location': '{location}'," "}}")).format( key=key.replace("'", "''"), location=location.replace("'", "''"), description=description.replace("'", "''"))) @err_to_scratch_buffer def list_snippets(self): """Shows the snippets that could be expanded to the User and let her select one.""" before = _vim.buf.line_till_cursor snippets = self._snips(before, True) if len(snippets) == 0: self._handle_failure(self.backward_trigger) return True # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) if not snippets: return True snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) return True @err_to_scratch_buffer def add_snippet(self, trigger, value, description, options, ft='all', priority=0, context=None, actions={}): """Add a snippet to the list of known snippets of the given 'ft'.""" self._added_snippets_source.add_snippet(ft, UltiSnipsSnippetDefinition(priority, trigger, value, description, options, {}, 'added', context, actions)) @err_to_scratch_buffer def expand_anon( self, value, trigger='', description='', options='', context=None, actions={} ): """Expand an anonymous snippet right here.""" before = _vim.buf.line_till_cursor snip = UltiSnipsSnippetDefinition(0, trigger, value, description, options, {}, '', context, actions) if not trigger or snip.matches(before, self._visual_content): self._do_snippet(snip, before) return True else: return False def register_snippet_source(self, name, snippet_source): """Registers a new 'snippet_source' with the given 'name'. The given class must be an instance of SnippetSource. This source will be queried for snippets. """ self._snippet_sources.append((name, snippet_source)) def unregister_snippet_source(self, name): """Unregister the source with the given 'name'. Does nothing if it is not registered. """ for index, (source_name, _) in enumerate(self._snippet_sources): if name == source_name: self._snippet_sources = self._snippet_sources[:index] + \ self._snippet_sources[index + 1:] break def reset_buffer_filetypes(self): """Reset the filetypes for the current buffer.""" if _vim.buf.number in self._buffer_filetypes: del self._buffer_filetypes[_vim.buf.number] def add_buffer_filetypes(self, ft): """Checks for changes in the list of snippet files or the contents of the snippet files and reloads them if necessary.""" buf_fts = self._buffer_filetypes[_vim.buf.number] idx = -1 for ft in ft.split('.'): ft = ft.strip() if not ft: continue try: idx = buf_fts.index(ft) except ValueError: self._buffer_filetypes[_vim.buf.number].insert(idx + 1, ft) idx += 1 @err_to_scratch_buffer def _cursor_moved(self): """Called whenever the cursor moved.""" self._should_update_textobjects = False if not self._csnippets and self._inner_state_up: self._teardown_inner_state() self._vstate.remember_position() if _vim.eval('mode()') not in 'in': return if self._ignore_movements: self._ignore_movements = False return if self._csnippets: cstart = self._csnippets[0].start.line cend = self._csnippets[0].end.line + \ self._vstate.diff_in_buffer_length ct = _vim.buf[cstart:cend + 1] lt = self._vstate.remembered_buffer pos = _vim.buf.cursor lt_span = [0, len(lt)] ct_span = [0, len(ct)] initial_line = cstart # Cut down on lines searched for changes. Start from behind and # remove all equal lines. Then do the same from the front. if lt and ct: while (lt[lt_span[1] - 1] == ct[ct_span[1] - 1] and self._vstate.ppos.line < initial_line + lt_span[1] - 1 and pos.line < initial_line + ct_span[1] - 1 and (lt_span[0] < lt_span[1]) and (ct_span[0] < ct_span[1])): ct_span[1] -= 1 lt_span[1] -= 1 while (lt_span[0] < lt_span[1] and ct_span[0] < ct_span[1] and lt[lt_span[0]] == ct[ct_span[0]] and self._vstate.ppos.line >= initial_line and pos.line >= initial_line): ct_span[0] += 1 lt_span[0] += 1 initial_line += 1 ct_span[0] = max(0, ct_span[0] - 1) lt_span[0] = max(0, lt_span[0] - 1) initial_line = max(cstart, initial_line - 1) lt = lt[lt_span[0]:lt_span[1]] ct = ct[ct_span[0]:ct_span[1]] try: rv, es = guess_edit(initial_line, lt, ct, self._vstate) if not rv: lt = '\n'.join(lt) ct = '\n'.join(ct) es = diff(lt, ct, initial_line) self._csnippets[0].replay_user_edits(es, self._ctab) except IndexError: # Rather do nothing than throwing an error. It will be correct # most of the time pass self._check_if_still_inside_snippet() if self._csnippets: self._csnippets[0].update_textobjects() self._vstate.remember_buffer(self._csnippets[0]) def _setup_inner_state(self): """Map keys and create autocommands that should only be defined when a snippet is active.""" if self._inner_state_up: return if self.expand_trigger != self.forward_trigger: _vim.command('inoremap <buffer> <silent> ' + self.forward_trigger + ' <C-R>=UltiSnips#JumpForwards()<cr>') _vim.command('snoremap <buffer> <silent> ' + self.forward_trigger + ' <Esc>:call UltiSnips#JumpForwards()<cr>') _vim.command('inoremap <buffer> <silent> ' + self.backward_trigger + ' <C-R>=UltiSnips#JumpBackwards()<cr>') _vim.command('snoremap <buffer> <silent> ' + self.backward_trigger + ' <Esc>:call UltiSnips#JumpBackwards()<cr>') # Setup the autogroups. _vim.command('augroup UltiSnips') _vim.command('autocmd!') _vim.command('autocmd CursorMovedI * call UltiSnips#CursorMoved()') _vim.command('autocmd CursorMoved * call UltiSnips#CursorMoved()') _vim.command( 'autocmd InsertLeave * call UltiSnips#LeavingInsertMode()') _vim.command('autocmd BufLeave * call UltiSnips#LeavingBuffer()') _vim.command( 'autocmd CmdwinEnter * call UltiSnips#LeavingBuffer()') _vim.command( 'autocmd CmdwinLeave * call UltiSnips#LeavingBuffer()') # Also exit the snippet when we enter a unite complete buffer. _vim.command('autocmd Filetype unite call UltiSnips#LeavingBuffer()') _vim.command('augroup END') _vim.command('silent doautocmd <nomodeline> User UltiSnipsEnterFirstSnippet') self._inner_state_up = True def _teardown_inner_state(self): """Reverse _setup_inner_state.""" if not self._inner_state_up: return try: _vim.command('silent doautocmd <nomodeline> User UltiSnipsExitLastSnippet') if self.expand_trigger != self.forward_trigger: _vim.command('iunmap <buffer> %s' % self.forward_trigger) _vim.command('sunmap <buffer> %s' % self.forward_trigger) _vim.command('iunmap <buffer> %s' % self.backward_trigger) _vim.command('sunmap <buffer> %s' % self.backward_trigger) _vim.command('augroup UltiSnips') _vim.command('autocmd!') _vim.command('augroup END') self._inner_state_up = False except _vim.error: # This happens when a preview window was opened. This issues # CursorMoved, but not BufLeave. We have no way to unmap, until we # are back in our buffer pass @err_to_scratch_buffer def _save_last_visual_selection(self): """This is called when the expand trigger is pressed in visual mode. Our job is to remember everything between '< and '> and pass it on to. ${VISUAL} in case it will be needed. """ self._visual_content.conserve() def _leaving_buffer(self): """Called when the user switches tabs/windows/buffers. It basically means that all snippets must be properly terminated. """ while len(self._csnippets): self._current_snippet_is_done() self._reinit() def _reinit(self): """Resets transient state.""" self._ctab = None self._ignore_movements = False def _check_if_still_inside_snippet(self): """Checks if the cursor is outside of the current snippet.""" if self._cs and ( not self._cs.start <= _vim.buf.cursor <= self._cs.end ): self._current_snippet_is_done() self._reinit() self._check_if_still_inside_snippet() def _current_snippet_is_done(self): """The current snippet should be terminated.""" self._csnippets.pop() if not self._csnippets: self._teardown_inner_state() def _jump(self, backwards=False): """Helper method that does the actual jump.""" if self._should_update_textobjects: self._should_reset_visual = False self._cursor_moved() # we need to set 'onemore' there, because of limitations of the vim # API regarding cursor movements; without that test # 'CanExpandAnonSnippetInJumpActionWhileSelected' will fail with _vim.toggle_opt('ve', 'onemore'): jumped = False # We need to remember current snippets stack here because of # post-jump action on the last tabstop should be able to access # snippet instance which is ended just now. stack_for_post_jump = self._csnippets[:] # If next tab has length 1 and the distance between itself and # self._ctab is 1 then there is 1 less CursorMove events. We # cannot ignore next movement in such case. ntab_short_and_near = False if self._cs: snippet_for_action = self._cs elif stack_for_post_jump: snippet_for_action = stack_for_post_jump[-1] else: snippet_for_action = None if self._cs: ntab = self._cs.select_next_tab(backwards) if ntab: if self._cs.snippet.has_option('s'): lineno = _vim.buf.cursor.line _vim.buf[lineno] = _vim.buf[lineno].rstrip() _vim.select(ntab.start, ntab.end) jumped = True if (self._ctab is not None and ntab.start - self._ctab.end == Position(0, 1) and ntab.end - ntab.start == Position(0, 1)): ntab_short_and_near = True self._ctab = ntab # Run interpolations again to update new placeholder # values, binded to currently newly jumped placeholder. self._visual_content.conserve_placeholder(self._ctab) self._cs.current_placeholder = \ self._visual_content.placeholder self._should_reset_visual = False self._csnippets[0].update_textobjects() self._vstate.remember_buffer(self._csnippets[0]) if ntab.number == 0 and self._csnippets: self._current_snippet_is_done() else: # This really shouldn't happen, because a snippet should # have been popped when its final tabstop was used. # Cleanup by removing current snippet and recursing. self._current_snippet_is_done() jumped = self._jump(backwards) if jumped: if self._ctab: self._vstate.remember_position() self._vstate.remember_unnamed_register(self._ctab.current_text) if not ntab_short_and_near: self._ignore_movements = True if len(stack_for_post_jump) > 0 and ntab is not None: with use_proxy_buffer(stack_for_post_jump, self._vstate): snippet_for_action.snippet.do_post_jump( ntab.number, -1 if backwards else 1, stack_for_post_jump, snippet_for_action ) return jumped def _leaving_insert_mode(self): """Called whenever we leave the insert mode.""" self._vstate.restore_unnamed_register() def _handle_failure(self, trigger): """Mainly make sure that we play well with SuperTab.""" if trigger.lower() == '<tab>': feedkey = '\\' + trigger elif trigger.lower() == '<s-tab>': feedkey = '\\' + trigger else: feedkey = None mode = 'n' if not self._supertab_keys: if _vim.eval("exists('g:SuperTabMappingForward')") != '0': self._supertab_keys = ( _vim.eval('g:SuperTabMappingForward'), _vim.eval('g:SuperTabMappingBackward'), ) else: self._supertab_keys = ['', ''] for idx, sttrig in enumerate(self._supertab_keys): if trigger.lower() == sttrig.lower(): if idx == 0: feedkey = r"\<Plug>SuperTabForward" mode = 'n' elif idx == 1: feedkey = r"\<Plug>SuperTabBackward" mode = 'p' # Use remap mode so SuperTab mappings will be invoked. break if (feedkey == r"\<Plug>SuperTabForward" or feedkey == r"\<Plug>SuperTabBackward"): _vim.command('return SuperTab(%s)' % _vim.escape(mode)) elif feedkey: _vim.command('return %s' % _vim.escape(feedkey)) def _snips(self, before, partial, autotrigger_only=False): """Returns all the snippets for the given text before the cursor. If partial is True, then get also return partial matches. """ filetypes = self._buffer_filetypes[_vim.buf.number][::-1] matching_snippets = defaultdict(list) clear_priority = None cleared = {} for _, source in self._snippet_sources: source.ensure(filetypes, cached=autotrigger_only) # Collect cleared information from sources. for _, source in self._snippet_sources: sclear_priority = source.get_clear_priority(filetypes) if sclear_priority is not None and (clear_priority is None or sclear_priority > clear_priority): clear_priority = sclear_priority for key, value in source.get_cleared(filetypes).items(): if key not in cleared or value > cleared[key]: cleared[key] = value for _, source in self._snippet_sources: possible_snippets = source.get_snippets( filetypes, before, partial, autotrigger_only, self._visual_content ) for snippet in possible_snippets: if ((clear_priority is None or snippet.priority > clear_priority) and (snippet.trigger not in cleared or snippet.priority > cleared[snippet.trigger])): matching_snippets[snippet.trigger].append(snippet) if not matching_snippets: return [] # Now filter duplicates and only keep the one with the highest # priority. snippets = [] for snippets_with_trigger in matching_snippets.values(): highest_priority = max(s.priority for s in snippets_with_trigger) snippets.extend(s for s in snippets_with_trigger if s.priority == highest_priority) # For partial matches we are done, but if we want to expand a snippet, # we have to go over them again and only keep those with the maximum # priority. if partial: return snippets highest_priority = max(s.priority for s in snippets) return [s for s in snippets if s.priority == highest_priority] def _do_snippet(self, snippet, before): """Expands the given snippet, and handles everything that needs to be done with it.""" self._setup_inner_state() self._snip_expanded_in_action = False self._should_update_textobjects = False # Adjust before, maybe the trigger is not the complete word text_before = before if snippet.matched: text_before = before[:-len(snippet.matched)] with use_proxy_buffer(self._csnippets, self._vstate): with self._action_context(): cursor_set_in_action = snippet.do_pre_expand( self._visual_content.text, self._csnippets ) if cursor_set_in_action: text_before = _vim.buf.line_till_cursor before = _vim.buf.line_till_cursor with suspend_proxy_edits(): if self._cs: start = Position(_vim.buf.cursor.line, len(text_before)) end = Position(_vim.buf.cursor.line, len(before)) # If cursor is set in pre-action, then action was modified # cursor line, in that case we do not need to do any edits, it # can break snippet if not cursor_set_in_action: # It could be that our trigger contains the content of # TextObjects in our containing snippet. If this is indeed # the case, we have to make sure that those are properly # killed. We do this by pretending that the user deleted # and retyped the text that our trigger matched. edit_actions = [ ('D', start.line, start.col, snippet.matched), ('I', start.line, start.col, snippet.matched), ] self._csnippets[0].replay_user_edits(edit_actions) si = snippet.launch(text_before, self._visual_content, self._cs.find_parent_for_new_to(start), start, end ) else: start = Position(_vim.buf.cursor.line, len(text_before)) end = Position(_vim.buf.cursor.line, len(before)) si = snippet.launch(text_before, self._visual_content, None, start, end) self._visual_content.reset() self._csnippets.append(si) si.update_textobjects() with use_proxy_buffer(self._csnippets, self._vstate): with self._action_context(): snippet.do_post_expand( si._start, si._end, self._csnippets ) self._vstate.remember_buffer(self._csnippets[0]) if not self._snip_expanded_in_action: self._jump() elif self._cs.current_text != '': self._jump() else: self._current_snippet_is_done() if self._inside_action: self._snip_expanded_in_action = True def _try_expand(self, autotrigger_only=False): """Try to expand a snippet in the current place.""" before = _vim.buf.line_till_cursor snippets = self._snips(before, False, autotrigger_only) if snippets: # prefer snippets with context if any snippets_with_context = [s for s in snippets if s.context] if snippets_with_context: snippets = snippets_with_context if not snippets: # No snippet found return False _vim.command('let &undolevels = &undolevels') if len(snippets) == 1: snippet = snippets[0] else: snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) _vim.command('let &undolevels = &undolevels') return True @property def _cs(self): """The current snippet or None.""" if not len(self._csnippets): return None return self._csnippets[-1] def _file_to_edit(self, requested_ft, bang): # pylint: disable=no-self-use """Returns a file to be edited for the given requested_ft. If 'bang' is empty only private files in g:UltiSnipsSnippetsDir are considered, otherwise all files are considered and the user gets to choose. """ # This method is not using self, but is called by UltiSnips.vim and is # therefore in this class because it is the facade to Vim. potentials = set() if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == '1': snippet_dir = _vim.eval('g:UltiSnipsSnippetsDir') else: home = _vim.eval('$HOME') if platform.system() == 'Windows': snippet_dir = os.path.join(home, 'vimfiles', 'UltiSnips') elif _vim.eval("has('nvim')") == '1': xdg_home_config = _vim.eval('$XDG_CONFIG_HOME') or os.path.join(home, ".config") snippet_dir = os.path.join(xdg_home_config, 'nvim', 'UltiSnips') else: snippet_dir = os.path.join(home, '.vim', 'UltiSnips') filetypes = [] if requested_ft: filetypes.append(requested_ft) else: if bang: filetypes.extend(self._buffer_filetypes[_vim.buf.number]) else: filetypes.append(self._buffer_filetypes[_vim.buf.number][0]) for ft in filetypes: potentials.update(find_snippet_files(ft, snippet_dir)) potentials.add(os.path.join(snippet_dir, ft + '.snippets')) if bang: potentials.update(find_all_snippet_files(ft)) potentials = set(os.path.realpath(os.path.expanduser(p)) for p in potentials) if len(potentials) > 1: files = sorted(potentials) formatted = [as_unicode('%i: %s') % (i, escape(fn, '\\')) for i, fn in enumerate(files, 1)] file_to_edit = _ask_user(files, formatted) if file_to_edit is None: return '' else: file_to_edit = potentials.pop() dirname = os.path.dirname(file_to_edit) if not os.path.exists(dirname): os.makedirs(dirname) return file_to_edit @contextmanager def _action_context(self): try: old_flag = self._inside_action self._inside_action = True yield finally: self._inside_action = old_flag @err_to_scratch_buffer def _track_change(self): self._should_update_textobjects = True inserted_char = _vim.eval('v:char') try: if inserted_char == '': before = _vim.buf.line_till_cursor if before and before[-1] == self._last_inserted_char: self._try_expand(autotrigger_only=True) finally: self._last_inserted_char = inserted_char if self._should_reset_visual and self._visual_content.mode == '': self._visual_content.reset() self._should_reset_visual = True
class SnippetManager(object): """The main entry point for all UltiSnips functionality. All Vim functions call methods in this class.""" def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_mappings_in_place = False self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source("ultisnips_files", UltiSnipsFileSource()) self.register_snippet_source("added", self._added_snippets_source) self.register_snippet_source("snipmate_files", SnipMateFileSource()) self._reinit() @err_to_scratch_buffer def jump_forwards(self): """Jumps to the next tabstop.""" _vim.command("let g:ulti_jump_forwards_res = 1") if not self._jump(): _vim.command("let g:ulti_jump_forwards_res = 0") return self._handle_failure(self.forward_trigger) @err_to_scratch_buffer def jump_backwards(self): """Jumps to the previous tabstop.""" _vim.command("let g:ulti_jump_backwards_res = 1") if not self._jump(True): _vim.command("let g:ulti_jump_backwards_res = 0") return self._handle_failure(self.backward_trigger) @err_to_scratch_buffer def expand(self): """Try to expand a snippet at the current position.""" _vim.command("let g:ulti_expand_res = 1") if not self._try_expand(): _vim.command("let g:ulti_expand_res = 0") self._handle_failure(self.expand_trigger) @err_to_scratch_buffer def expand_or_jump(self): """ This function is used for people who wants to have the same trigger for expansion and forward jumping. It first tries to expand a snippet, if this fails, it tries to jump forward. """ _vim.command('let g:ulti_expand_or_jump_res = 1') rv = self._try_expand() if not rv: _vim.command('let g:ulti_expand_or_jump_res = 2') rv = self._jump() if not rv: _vim.command('let g:ulti_expand_or_jump_res = 0') self._handle_failure(self.expand_trigger) @err_to_scratch_buffer def snippets_in_current_scope(self): """Returns the snippets that could be expanded to Vim as a global variable.""" before = _vim.buf.line_till_cursor snippets = self._snips(before, True) # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) for snip in snippets: description = snip.description[snip.description.find(snip.trigger ) + len(snip.trigger) + 2:] key = as_unicode(snip.trigger) description = as_unicode(description) # remove surrounding "" or '' in snippet description if it exists if len(description) > 2: if (description[0] == description[-1] and description[0] in "'\""): description = description[1:-1] _vim.command( as_unicode( "let g:current_ulti_dict['{key}'] = '{val}'").format( key=key.replace("'", "''"), val=description.replace("'", "''"))) @err_to_scratch_buffer def list_snippets(self): """Shows the snippets that could be expanded to the User and let her select one.""" before = _vim.buf.line_till_cursor snippets = self._snips(before, True) if len(snippets) == 0: self._handle_failure(self.backward_trigger) return True # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) if not snippets: return True snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) return True @err_to_scratch_buffer def add_snippet(self, trigger, value, description, options, ft="all", priority=0): """Add a snippet to the list of known snippets of the given 'ft'.""" self._added_snippets_source.add_snippet( ft, UltiSnipsSnippetDefinition(priority, trigger, value, description, options, {}, "added")) @err_to_scratch_buffer def expand_anon(self, value, trigger="", description="", options=""): """Expand an anonymous snippet right here.""" before = _vim.buf.line_till_cursor snip = UltiSnipsSnippetDefinition(0, trigger, value, description, options, {}, "") if not trigger or snip.matches(before): self._do_snippet(snip, before) return True else: return False def register_snippet_source(self, name, snippet_source): """Registers a new 'snippet_source' with the given 'name'. The given class must be an instance of SnippetSource. This source will be queried for snippets.""" self._snippet_sources.append((name, snippet_source)) def unregister_snippet_source(self, name): """Unregister the source with the given 'name'. Does nothing if it is not registered.""" for index, (source_name, _) in enumerate(self._snippet_sources): if name == source_name: self._snippet_sources = self._snippet_sources[:index] + \ self._snippet_sources[index+1:] break def reset_buffer_filetypes(self): """Reset the filetypes for the current buffer.""" if _vim.buf.number in self._buffer_filetypes: del self._buffer_filetypes[_vim.buf.number] def add_buffer_filetypes(self, ft): """Checks for changes in the list of snippet files or the contents of the snippet files and reloads them if necessary. """ buf_fts = self._buffer_filetypes[_vim.buf.number] idx = -1 for ft in ft.split("."): ft = ft.strip() if not ft: continue try: idx = buf_fts.index(ft) except ValueError: self._buffer_filetypes[_vim.buf.number].insert(idx + 1, ft) idx += 1 @err_to_scratch_buffer def _cursor_moved(self): """Called whenever the cursor moved.""" if not self._csnippets and self._inner_mappings_in_place: self._unmap_inner_keys() self._vstate.remember_position() if _vim.eval("mode()") not in 'in': return if self._ignore_movements: self._ignore_movements = False return if self._csnippets: cstart = self._csnippets[0].start.line cend = self._csnippets[0].end.line + \ self._vstate.diff_in_buffer_length ct = _vim.buf[cstart:cend + 1] lt = self._vstate.remembered_buffer pos = _vim.buf.cursor lt_span = [0, len(lt)] ct_span = [0, len(ct)] initial_line = cstart # Cut down on lines searched for changes. Start from behind and # remove all equal lines. Then do the same from the front. if lt and ct: while (lt[lt_span[1] - 1] == ct[ct_span[1] - 1] and self._vstate.ppos.line < initial_line + lt_span[1] - 1 and pos.line < initial_line + ct_span[1] - 1 and (lt_span[0] < lt_span[1]) and (ct_span[0] < ct_span[1])): ct_span[1] -= 1 lt_span[1] -= 1 while (lt_span[0] < lt_span[1] and ct_span[0] < ct_span[1] and lt[lt_span[0]] == ct[ct_span[0]] and self._vstate.ppos.line >= initial_line and pos.line >= initial_line): ct_span[0] += 1 lt_span[0] += 1 initial_line += 1 ct_span[0] = max(0, ct_span[0] - 1) lt_span[0] = max(0, lt_span[0] - 1) initial_line = max(cstart, initial_line - 1) lt = lt[lt_span[0]:lt_span[1]] ct = ct[ct_span[0]:ct_span[1]] try: rv, es = guess_edit(initial_line, lt, ct, self._vstate) if not rv: lt = '\n'.join(lt) ct = '\n'.join(ct) es = diff(lt, ct, initial_line) self._csnippets[0].replay_user_edits(es) except IndexError: # Rather do nothing than throwing an error. It will be correct # most of the time pass self._check_if_still_inside_snippet() if self._csnippets: self._csnippets[0].update_textobjects() self._vstate.remember_buffer(self._csnippets[0]) def _map_inner_keys(self): """Map keys that should only be defined when a snippet is active.""" if self.expand_trigger != self.forward_trigger: _vim.command("inoremap <buffer> <silent> " + self.forward_trigger + " <C-R>=UltiSnips#JumpForwards()<cr>") _vim.command("snoremap <buffer> <silent> " + self.forward_trigger + " <Esc>:call UltiSnips#JumpForwards()<cr>") _vim.command("inoremap <buffer> <silent> " + self.backward_trigger + " <C-R>=UltiSnips#JumpBackwards()<cr>") _vim.command("snoremap <buffer> <silent> " + self.backward_trigger + " <Esc>:call UltiSnips#JumpBackwards()<cr>") self._inner_mappings_in_place = True def _unmap_inner_keys(self): """Unmap keys that should not be active when no snippet is active.""" if not self._inner_mappings_in_place: return try: if self.expand_trigger != self.forward_trigger: _vim.command("iunmap <buffer> %s" % self.forward_trigger) _vim.command("sunmap <buffer> %s" % self.forward_trigger) _vim.command("iunmap <buffer> %s" % self.backward_trigger) _vim.command("sunmap <buffer> %s" % self.backward_trigger) self._inner_mappings_in_place = False except _vim.error: # This happens when a preview window was opened. This issues # CursorMoved, but not BufLeave. We have no way to unmap, until we # are back in our buffer pass @err_to_scratch_buffer def _save_last_visual_selection(self): """ This is called when the expand trigger is pressed in visual mode. Our job is to remember everything between '< and '> and pass it on to ${VISUAL} in case it will be needed. """ self._visual_content.conserve() def _leaving_buffer(self): """Called when the user switches tabs/windows/buffers. It basically means that all snippets must be properly terminated.""" while len(self._csnippets): self._current_snippet_is_done() self._reinit() def _reinit(self): """Resets transient state.""" self._ctab = None self._ignore_movements = False def _check_if_still_inside_snippet(self): """Checks if the cursor is outside of the current snippet.""" if self._cs and ( not self._cs.start <= _vim.buf.cursor <= self._cs.end): self._current_snippet_is_done() self._reinit() self._check_if_still_inside_snippet() def _current_snippet_is_done(self): """The current snippet should be terminated.""" self._csnippets.pop() if not self._csnippets: self._unmap_inner_keys() def _jump(self, backwards=False): """Helper method that does the actual jump.""" jumped = False if self._cs: self._ctab = self._cs.select_next_tab(backwards) if self._ctab: if self._cs.snippet.has_option("s"): lineno = _vim.buf.cursor.line _vim.buf[lineno] = _vim.buf[lineno].rstrip() _vim.select(self._ctab.start, self._ctab.end) jumped = True if self._ctab.number == 0: self._current_snippet_is_done() else: # This really shouldn't happen, because a snippet should # have been popped when its final tabstop was used. # Cleanup by removing current snippet and recursing. self._current_snippet_is_done() jumped = self._jump(backwards) if jumped: self._vstate.remember_position() self._vstate.remember_unnamed_register(self._ctab.current_text) self._ignore_movements = True return jumped def _leaving_insert_mode(self): """Called whenever we leave the insert mode.""" self._vstate.restore_unnamed_register() def _handle_failure(self, trigger): """Mainly make sure that we play well with SuperTab.""" if trigger.lower() == "<tab>": feedkey = "\\" + trigger elif trigger.lower() == "<s-tab>": feedkey = "\\" + trigger else: feedkey = None mode = "n" if not self._supertab_keys: if _vim.eval("exists('g:SuperTabMappingForward')") != "0": self._supertab_keys = ( _vim.eval("g:SuperTabMappingForward"), _vim.eval("g:SuperTabMappingBackward"), ) else: self._supertab_keys = ['', ''] for idx, sttrig in enumerate(self._supertab_keys): if trigger.lower() == sttrig.lower(): if idx == 0: feedkey = r"\<Plug>SuperTabForward" mode = "n" elif idx == 1: feedkey = r"\<Plug>SuperTabBackward" mode = "p" # Use remap mode so SuperTab mappings will be invoked. break if (feedkey == r"\<Plug>SuperTabForward" or feedkey == r"\<Plug>SuperTabBackward"): _vim.command("return SuperTab(%s)" % _vim.escape(mode)) elif feedkey: _vim.command("return %s" % _vim.escape(feedkey)) def _snips(self, before, partial): """Returns all the snippets for the given text before the cursor. If partial is True, then get also return partial matches. """ filetypes = self._buffer_filetypes[_vim.buf.number][::-1] matching_snippets = defaultdict(list) for _, source in self._snippet_sources: for snippet in source.get_snippets(filetypes, before, partial): matching_snippets[snippet.trigger].append(snippet) if not matching_snippets: return [] # Now filter duplicates and only keep the one with the highest # priority. snippets = [] for snippets_with_trigger in matching_snippets.values(): highest_priority = max(s.priority for s in snippets_with_trigger) snippets.extend(s for s in snippets_with_trigger if s.priority == highest_priority) # For partial matches we are done, but if we want to expand a snippet, # we have to go over them again and only keep those with the maximum # priority. if partial: return snippets highest_priority = max(s.priority for s in snippets) return [s for s in snippets if s.priority == highest_priority] def _do_snippet(self, snippet, before): """Expands the given snippet, and handles everything that needs to be done with it.""" self._map_inner_keys() # Adjust before, maybe the trigger is not the complete word text_before = before if snippet.matched: text_before = before[:-len(snippet.matched)] if self._cs: start = Position(_vim.buf.cursor.line, len(text_before)) end = Position(_vim.buf.cursor.line, len(before)) # It could be that our trigger contains the content of TextObjects # in our containing snippet. If this is indeed the case, we have to # make sure that those are properly killed. We do this by # pretending that the user deleted and retyped the text that our # trigger matched. edit_actions = [ ("D", start.line, start.col, snippet.matched), ("I", start.line, start.col, snippet.matched), ] self._csnippets[0].replay_user_edits(edit_actions) si = snippet.launch(text_before, self._visual_content, self._cs.find_parent_for_new_to(start), start, end) else: start = Position(_vim.buf.cursor.line, len(text_before)) end = Position(_vim.buf.cursor.line, len(before)) si = snippet.launch(text_before, self._visual_content, None, start, end) self._visual_content.reset() self._csnippets.append(si) si.update_textobjects() self._ignore_movements = True self._vstate.remember_buffer(self._csnippets[0]) self._jump() def _try_expand(self): """Try to expand a snippet in the current place.""" before = _vim.buf.line_till_cursor if not before: return False snippets = self._snips(before, False) if not snippets: # No snippet found return False elif len(snippets) == 1: snippet = snippets[0] else: snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) return True @property def _cs(self): """The current snippet or None.""" if not len(self._csnippets): return None return self._csnippets[-1] def _file_to_edit(self, requested_ft, bang): # pylint: disable=no-self-use """Returns a file to be edited for the given requested_ft. If 'bang' is empty only private files in g:UltiSnipsSnippetsDir are considered, otherwise all files are considered and the user gets to choose. """ # This method is not using self, but is called by UltiSnips.vim and is # therefore in this class because it is the facade to Vim. potentials = set() if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == "1": snippet_dir = _vim.eval("g:UltiSnipsSnippetsDir") else: if platform.system() == "Windows": snippet_dir = os.path.join(_vim.eval("$HOME"), "_vimfiles", "UltiSnips") else: snippet_dir = os.path.join(_vim.eval("$HOME"), ".vim", "UltiSnips") filetypes = [] if requested_ft: filetypes.append(requested_ft) else: if bang: filetypes.extend(self._buffer_filetypes[_vim.buf.number]) else: filetypes.append(self._buffer_filetypes[_vim.buf.number][0]) for ft in filetypes: potentials.update(find_snippet_files(ft, snippet_dir)) potentials.add(os.path.join(snippet_dir, ft + '.snippets')) if bang: potentials.update(find_all_snippet_files(ft)) potentials = set( os.path.realpath(os.path.expanduser(p)) for p in potentials) if len(potentials) > 1: files = sorted(potentials) formatted = [ as_unicode('%i: %s') % (i, fn) for i, fn in enumerate(files, 1) ] file_to_edit = _ask_user(files, formatted) if file_to_edit is None: return "" else: file_to_edit = potentials.pop() dirname = os.path.dirname(file_to_edit) if not os.path.exists(dirname): os.makedirs(dirname) return file_to_edit
class SnippetManager(object): """The main entry point for all UltiSnips functionality. All Vim functions call methods in this class.""" def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_mappings_in_place = False self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source("ultisnips_files", UltiSnipsFileSource()) self.register_snippet_source("added", self._added_snippets_source) self.register_snippet_source("snipmate_files", SnipMateFileSource()) self._reinit() @err_to_scratch_buffer def jump_forwards(self): """Jumps to the next tabstop.""" _vim.command("let g:ulti_jump_forwards_res = 1") if not self._jump(): _vim.command("let g:ulti_jump_forwards_res = 0") return self._handle_failure(self.forward_trigger) @err_to_scratch_buffer def jump_backwards(self): """Jumps to the previous tabstop.""" _vim.command("let g:ulti_jump_backwards_res = 1") if not self._jump(True): _vim.command("let g:ulti_jump_backwards_res = 0") return self._handle_failure(self.backward_trigger) @err_to_scratch_buffer def expand(self): """Try to expand a snippet at the current position.""" _vim.command("let g:ulti_expand_res = 1") if not self._try_expand(): _vim.command("let g:ulti_expand_res = 0") self._handle_failure(self.expand_trigger) @err_to_scratch_buffer def expand_or_jump(self): """ This function is used for people who wants to have the same trigger for expansion and forward jumping. It first tries to expand a snippet, if this fails, it tries to jump forward. """ _vim.command('let g:ulti_expand_or_jump_res = 1') rv = self._try_expand() if not rv: _vim.command('let g:ulti_expand_or_jump_res = 2') rv = self._jump() if not rv: _vim.command('let g:ulti_expand_or_jump_res = 0') self._handle_failure(self.expand_trigger) @err_to_scratch_buffer def snippets_in_current_scope(self): """Returns the snippets that could be expanded to Vim as a global variable.""" before = _vim.buf.line_till_cursor snippets = self._snips(before, True) # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) for snip in snippets: description = snip.description[snip.description.find(snip.trigger) + len(snip.trigger) + 2:] key = as_unicode(snip.trigger) description = as_unicode(description) # remove surrounding "" or '' in snippet description if it exists if len(description) > 2: if (description[0] == description[-1] and description[0] in "'\""): description = description[1:-1] _vim.command(as_unicode( "let g:current_ulti_dict['{key}'] = '{val}'").format( key=key.replace("'", "''"), val=description.replace("'", "''"))) @err_to_scratch_buffer def list_snippets(self): """Shows the snippets that could be expanded to the User and let her select one.""" before = _vim.buf.line_till_cursor snippets = self._snips(before, True) if len(snippets) == 0: self._handle_failure(self.backward_trigger) return True # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) if not snippets: return True snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) return True @err_to_scratch_buffer def add_snippet(self, trigger, value, description, options, ft="all", priority=0): """Add a snippet to the list of known snippets of the given 'ft'.""" self._added_snippets_source.add_snippet(ft, UltiSnipsSnippetDefinition(priority, trigger, value, description, options, {}, "added")) @err_to_scratch_buffer def expand_anon(self, value, trigger="", description="", options=""): """Expand an anonymous snippet right here.""" before = _vim.buf.line_till_cursor snip = UltiSnipsSnippetDefinition(0, trigger, value, description, options, {}, "") if not trigger or snip.matches(before): self._do_snippet(snip, before) return True else: return False def register_snippet_source(self, name, snippet_source): """Registers a new 'snippet_source' with the given 'name'. The given class must be an instance of SnippetSource. This source will be queried for snippets.""" self._snippet_sources.append((name, snippet_source)) def unregister_snippet_source(self, name): """Unregister the source with the given 'name'. Does nothing if it is not registered.""" for index, (source_name, _) in enumerate(self._snippet_sources): if name == source_name: self._snippet_sources = self._snippet_sources[:index] + \ self._snippet_sources[index+1:] break def reset_buffer_filetypes(self): """Reset the filetypes for the current buffer.""" if _vim.buf.number in self._buffer_filetypes: del self._buffer_filetypes[_vim.buf.number] def add_buffer_filetypes(self, ft): """Checks for changes in the list of snippet files or the contents of the snippet files and reloads them if necessary. """ buf_fts = self._buffer_filetypes[_vim.buf.number] idx = -1 for ft in ft.split("."): ft = ft.strip() if not ft: continue try: idx = buf_fts.index(ft) except ValueError: self._buffer_filetypes[_vim.buf.number].insert(idx + 1, ft) idx += 1 @err_to_scratch_buffer def _cursor_moved(self): """Called whenever the cursor moved.""" if not self._csnippets and self._inner_mappings_in_place: self._unmap_inner_keys() self._vstate.remember_position() if _vim.eval("mode()") not in 'in': return if self._ignore_movements: self._ignore_movements = False return if self._csnippets: cstart = self._csnippets[0].start.line cend = self._csnippets[0].end.line + \ self._vstate.diff_in_buffer_length ct = _vim.buf[cstart:cend + 1] lt = self._vstate.remembered_buffer pos = _vim.buf.cursor lt_span = [0, len(lt)] ct_span = [0, len(ct)] initial_line = cstart # Cut down on lines searched for changes. Start from behind and # remove all equal lines. Then do the same from the front. if lt and ct: while (lt[lt_span[1]-1] == ct[ct_span[1]-1] and self._vstate.ppos.line < initial_line + lt_span[1]-1 and pos.line < initial_line + ct_span[1]-1 and (lt_span[0] < lt_span[1]) and (ct_span[0] < ct_span[1])): ct_span[1] -= 1 lt_span[1] -= 1 while (lt_span[0] < lt_span[1] and ct_span[0] < ct_span[1] and lt[lt_span[0]] == ct[ct_span[0]] and self._vstate.ppos.line >= initial_line and pos.line >= initial_line): ct_span[0] += 1 lt_span[0] += 1 initial_line += 1 ct_span[0] = max(0, ct_span[0] - 1) lt_span[0] = max(0, lt_span[0] - 1) initial_line = max(cstart, initial_line - 1) lt = lt[lt_span[0]:lt_span[1]] ct = ct[ct_span[0]:ct_span[1]] try: rv, es = guess_edit(initial_line, lt, ct, self._vstate) if not rv: lt = '\n'.join(lt) ct = '\n'.join(ct) es = diff(lt, ct, initial_line) self._csnippets[0].replay_user_edits(es) except IndexError: # Rather do nothing than throwing an error. It will be correct # most of the time pass self._check_if_still_inside_snippet() if self._csnippets: self._csnippets[0].update_textobjects() self._vstate.remember_buffer(self._csnippets[0]) def _map_inner_keys(self): """Map keys that should only be defined when a snippet is active.""" if self.expand_trigger != self.forward_trigger: _vim.command("inoremap <buffer> <silent> " + self.forward_trigger + " <C-R>=UltiSnips#JumpForwards()<cr>") _vim.command("snoremap <buffer> <silent> " + self.forward_trigger + " <Esc>:call UltiSnips#JumpForwards()<cr>") _vim.command("inoremap <buffer> <silent> " + self.backward_trigger + " <C-R>=UltiSnips#JumpBackwards()<cr>") _vim.command("snoremap <buffer> <silent> " + self.backward_trigger + " <Esc>:call UltiSnips#JumpBackwards()<cr>") self._inner_mappings_in_place = True def _unmap_inner_keys(self): """Unmap keys that should not be active when no snippet is active.""" if not self._inner_mappings_in_place: return try: if self.expand_trigger != self.forward_trigger: _vim.command("iunmap <buffer> %s" % self.forward_trigger) _vim.command("sunmap <buffer> %s" % self.forward_trigger) _vim.command("iunmap <buffer> %s" % self.backward_trigger) _vim.command("sunmap <buffer> %s" % self.backward_trigger) self._inner_mappings_in_place = False except _vim.error: # This happens when a preview window was opened. This issues # CursorMoved, but not BufLeave. We have no way to unmap, until we # are back in our buffer pass @err_to_scratch_buffer def _save_last_visual_selection(self): """ This is called when the expand trigger is pressed in visual mode. Our job is to remember everything between '< and '> and pass it on to ${VISUAL} in case it will be needed. """ self._visual_content.conserve() def _leaving_buffer(self): """Called when the user switches tabs/windows/buffers. It basically means that all snippets must be properly terminated.""" while len(self._csnippets): self._current_snippet_is_done() self._reinit() def _reinit(self): """Resets transient state.""" self._ctab = None self._ignore_movements = False def _check_if_still_inside_snippet(self): """Checks if the cursor is outside of the current snippet.""" if self._cs and ( not self._cs.start <= _vim.buf.cursor <= self._cs.end ): self._current_snippet_is_done() self._reinit() self._check_if_still_inside_snippet() def _current_snippet_is_done(self): """The current snippet should be terminated.""" self._csnippets.pop() if not self._csnippets: self._unmap_inner_keys() def _jump(self, backwards=False): """Helper method that does the actual jump.""" jumped = False if self._cs: self._ctab = self._cs.select_next_tab(backwards) if self._ctab: if self._cs.snippet.has_option("s"): lineno = _vim.buf.cursor.line _vim.buf[lineno] = _vim.buf[lineno].rstrip() _vim.select(self._ctab.start, self._ctab.end) jumped = True if self._ctab.number == 0: self._current_snippet_is_done() else: # This really shouldn't happen, because a snippet should # have been popped when its final tabstop was used. # Cleanup by removing current snippet and recursing. self._current_snippet_is_done() jumped = self._jump(backwards) if jumped: self._vstate.remember_position() self._vstate.remember_unnamed_register(self._ctab.current_text) self._ignore_movements = True return jumped def _leaving_insert_mode(self): """Called whenever we leave the insert mode.""" self._vstate.restore_unnamed_register() def _handle_failure(self, trigger): """Mainly make sure that we play well with SuperTab.""" if trigger.lower() == "<tab>": feedkey = "\\" + trigger elif trigger.lower() == "<s-tab>": feedkey = "\\" + trigger else: feedkey = None mode = "n" if not self._supertab_keys: if _vim.eval("exists('g:SuperTabMappingForward')") != "0": self._supertab_keys = ( _vim.eval("g:SuperTabMappingForward"), _vim.eval("g:SuperTabMappingBackward"), ) else: self._supertab_keys = ['', ''] for idx, sttrig in enumerate(self._supertab_keys): if trigger.lower() == sttrig.lower(): if idx == 0: feedkey = r"\<Plug>SuperTabForward" mode = "n" elif idx == 1: feedkey = r"\<Plug>SuperTabBackward" mode = "p" # Use remap mode so SuperTab mappings will be invoked. break if (feedkey == r"\<Plug>SuperTabForward" or feedkey == r"\<Plug>SuperTabBackward"): _vim.command("return SuperTab(%s)" % _vim.escape(mode)) elif feedkey: _vim.command("return %s" % _vim.escape(feedkey)) def _snips(self, before, partial): """Returns all the snippets for the given text before the cursor. If partial is True, then get also return partial matches. """ filetypes = self._buffer_filetypes[_vim.buf.number][::-1] matching_snippets = defaultdict(list) for _, source in self._snippet_sources: for snippet in source.get_snippets(filetypes, before, partial): matching_snippets[snippet.trigger].append(snippet) if not matching_snippets: return [] # Now filter duplicates and only keep the one with the highest # priority. snippets = [] for snippets_with_trigger in matching_snippets.values(): highest_priority = max(s.priority for s in snippets_with_trigger) snippets.extend(s for s in snippets_with_trigger if s.priority == highest_priority) # For partial matches we are done, but if we want to expand a snippet, # we have to go over them again and only keep those with the maximum # priority. if partial: return snippets highest_priority = max(s.priority for s in snippets) return [s for s in snippets if s.priority == highest_priority] def _do_snippet(self, snippet, before): """Expands the given snippet, and handles everything that needs to be done with it.""" self._map_inner_keys() # Adjust before, maybe the trigger is not the complete word text_before = before if snippet.matched: text_before = before[:-len(snippet.matched)] if self._cs: start = Position(_vim.buf.cursor.line, len(text_before)) end = Position(_vim.buf.cursor.line, len(before)) # It could be that our trigger contains the content of TextObjects # in our containing snippet. If this is indeed the case, we have to # make sure that those are properly killed. We do this by # pretending that the user deleted and retyped the text that our # trigger matched. edit_actions = [ ("D", start.line, start.col, snippet.matched), ("I", start.line, start.col, snippet.matched), ] self._csnippets[0].replay_user_edits(edit_actions) si = snippet.launch(text_before, self._visual_content, self._cs.find_parent_for_new_to(start), start, end) else: start = Position(_vim.buf.cursor.line, len(text_before)) end = Position(_vim.buf.cursor.line, len(before)) si = snippet.launch(text_before, self._visual_content, None, start, end) self._visual_content.reset() self._csnippets.append(si) si.update_textobjects() self._ignore_movements = True self._vstate.remember_buffer(self._csnippets[0]) self._jump() def _try_expand(self): """Try to expand a snippet in the current place.""" before = _vim.buf.line_till_cursor if not before: return False snippets = self._snips(before, False) if not snippets: # No snippet found return False elif len(snippets) == 1: snippet = snippets[0] else: snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) return True @property def _cs(self): """The current snippet or None.""" if not len(self._csnippets): return None return self._csnippets[-1] def _file_to_edit(self, requested_ft, bang): # pylint: disable=no-self-use """Returns a file to be edited for the given requested_ft. If 'bang' is empty only private files in g:UltiSnipsSnippetsDir are considered, otherwise all files are considered and the user gets to choose. """ # This method is not using self, but is called by UltiSnips.vim and is # therefore in this class because it is the facade to Vim. potentials = set() if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == "1": snippet_dir = _vim.eval("g:UltiSnipsSnippetsDir") else: if platform.system() == "Windows": snippet_dir = os.path.join(_vim.eval("$HOME"), "vimfiles", "UltiSnips") else: snippet_dir = os.path.join(_vim.eval("$HOME"), ".vim", "UltiSnips") filetypes = [] if requested_ft: filetypes.append(requested_ft) else: if bang: filetypes.extend(self._buffer_filetypes[_vim.buf.number]) else: filetypes.append(self._buffer_filetypes[_vim.buf.number][0]) for ft in filetypes: potentials.update(find_snippet_files(ft, snippet_dir)) potentials.add(os.path.join(snippet_dir, ft + '.snippets')) if bang: potentials.update(find_all_snippet_files(ft)) potentials = set(os.path.realpath(os.path.expanduser(p)) for p in potentials) if len(potentials) > 1: files = sorted(potentials) formatted = [as_unicode('%i: %s') % (i, escape(fn, '\\')) for i, fn in enumerate(files, 1)] file_to_edit = _ask_user(files, formatted) if file_to_edit is None: return "" else: file_to_edit = potentials.pop() dirname = os.path.dirname(file_to_edit) if not os.path.exists(dirname): os.makedirs(dirname) return file_to_edit
class SnippetManager: """The main entry point for all UltiSnips functionality. All Vim functions call methods in this class. """ def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._inner_state_up = False self._supertab_keys = None self._active_snippets = [] self._added_buffer_filetypes = defaultdict(lambda: []) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._snippet_sources = [] self._snip_expanded_in_action = False self._inside_action = False self._last_change = ("", Position(-1, -1)) self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source("ultisnips_files", UltiSnipsFileSource()) self.register_snippet_source("added", self._added_snippets_source) enable_snipmate = "1" if vim_helper.eval("exists('g:UltiSnipsEnableSnipMate')") == "1": enable_snipmate = vim_helper.eval("g:UltiSnipsEnableSnipMate") if enable_snipmate == "1": self.register_snippet_source("snipmate_files", SnipMateFileSource()) self._should_update_textobjects = False self._should_reset_visual = False self._reinit() @err_to_scratch_buffer.wrap def jump_forwards(self): """Jumps to the next tabstop.""" vim_helper.command("let g:ulti_jump_forwards_res = 1") vim_helper.command("let &undolevels = &undolevels") if not self._jump(): vim_helper.command("let g:ulti_jump_forwards_res = 0") return self._handle_failure(self.forward_trigger) return None @err_to_scratch_buffer.wrap def jump_backwards(self): """Jumps to the previous tabstop.""" vim_helper.command("let g:ulti_jump_backwards_res = 1") vim_helper.command("let &undolevels = &undolevels") if not self._jump(True): vim_helper.command("let g:ulti_jump_backwards_res = 0") return self._handle_failure(self.backward_trigger) return None @err_to_scratch_buffer.wrap def expand(self): """Try to expand a snippet at the current position.""" vim_helper.command("let g:ulti_expand_res = 1") if not self._try_expand(): vim_helper.command("let g:ulti_expand_res = 0") self._handle_failure(self.expand_trigger) @err_to_scratch_buffer.wrap def expand_or_jump(self): """This function is used for people who wants to have the same trigger for expansion and forward jumping. It first tries to expand a snippet, if this fails, it tries to jump forward. """ vim_helper.command("let g:ulti_expand_or_jump_res = 1") rv = self._try_expand() if not rv: vim_helper.command("let g:ulti_expand_or_jump_res = 2") rv = self._jump() if not rv: vim_helper.command("let g:ulti_expand_or_jump_res = 0") self._handle_failure(self.expand_trigger) @err_to_scratch_buffer.wrap def snippets_in_current_scope(self, search_all): """Returns the snippets that could be expanded to Vim as a global variable.""" before = "" if search_all else vim_helper.buf.line_till_cursor snippets = self._snips(before, True) # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) for snip in snippets: description = snip.description[snip.description.find(snip.trigger ) + len(snip.trigger) + 2:] location = snip.location if snip.location else "" key = snip.trigger # remove surrounding "" or '' in snippet description if it exists if len(description) > 2: if description[0] == description[-1] and description[ 0] in "'\"": description = description[1:-1] vim_helper.command( "let g:current_ulti_dict['{key}'] = '{val}'".format( key=key.replace("'", "''"), val=description.replace("'", "''"))) if search_all: vim_helper.command( ("let g:current_ulti_dict_info['{key}'] = {{" "'description': '{description}'," "'location': '{location}'," "}}").format( key=key.replace("'", "''"), location=location.replace("'", "''"), description=description.replace("'", "''"), )) @err_to_scratch_buffer.wrap def list_snippets(self): """Shows the snippets that could be expanded to the User and let her select one.""" before = vim_helper.buf.line_till_cursor snippets = self._snips(before, True) if len(snippets) == 0: self._handle_failure(self.backward_trigger) return True # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) if not snippets: return True snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) return True @err_to_scratch_buffer.wrap def add_snippet( self, trigger, value, description, options, ft="all", priority=0, context=None, actions=None, ): """Add a snippet to the list of known snippets of the given 'ft'.""" self._added_snippets_source.add_snippet( ft, UltiSnipsSnippetDefinition( priority, trigger, value, description, options, {}, "added", context, actions, ), ) @err_to_scratch_buffer.wrap def expand_anon(self, value, trigger="", description="", options="", context=None, actions=None): """Expand an anonymous snippet right here.""" before = vim_helper.buf.line_till_cursor snip = UltiSnipsSnippetDefinition(0, trigger, value, description, options, {}, "", context, actions) if not trigger or snip.matches(before, self._visual_content): self._do_snippet(snip, before) return True return False def register_snippet_source(self, name, snippet_source): """Registers a new 'snippet_source' with the given 'name'. The given class must be an instance of SnippetSource. This source will be queried for snippets. """ self._snippet_sources.append((name, snippet_source)) def unregister_snippet_source(self, name): """Unregister the source with the given 'name'. Does nothing if it is not registered. """ for index, (source_name, _) in enumerate(self._snippet_sources): if name == source_name: self._snippet_sources = (self._snippet_sources[:index] + self._snippet_sources[index + 1:]) break def get_buffer_filetypes(self): return (self._added_buffer_filetypes[vim_helper.buf.number] + vim_helper.buf.filetypes + ["all"]) def add_buffer_filetypes(self, filetypes: str): """'filetypes' is a dotted filetype list, for example 'cuda.cpp'""" buf_fts = self._added_buffer_filetypes[vim_helper.buf.number] idx = -1 for ft in filetypes.split("."): ft = ft.strip() if not ft: continue try: idx = buf_fts.index(ft) except ValueError: self._added_buffer_filetypes[vim_helper.buf.number].insert( idx + 1, ft) idx += 1 @err_to_scratch_buffer.wrap def _cursor_moved(self): """Called whenever the cursor moved.""" self._should_update_textobjects = False self._vstate.remember_position() if vim_helper.eval("mode()") not in "in": return if self._ignore_movements: self._ignore_movements = False return if self._active_snippets: cstart = self._active_snippets[0].start.line cend = (self._active_snippets[0].end.line + self._vstate.diff_in_buffer_length) ct = vim_helper.buf[cstart:cend + 1] lt = self._vstate.remembered_buffer pos = vim_helper.buf.cursor lt_span = [0, len(lt)] ct_span = [0, len(ct)] initial_line = cstart # Cut down on lines searched for changes. Start from behind and # remove all equal lines. Then do the same from the front. if lt and ct: while (lt[lt_span[1] - 1] == ct[ct_span[1] - 1] and self._vstate.ppos.line < initial_line + lt_span[1] - 1 and pos.line < initial_line + ct_span[1] - 1 and (lt_span[0] < lt_span[1]) and (ct_span[0] < ct_span[1])): ct_span[1] -= 1 lt_span[1] -= 1 while (lt_span[0] < lt_span[1] and ct_span[0] < ct_span[1] and lt[lt_span[0]] == ct[ct_span[0]] and self._vstate.ppos.line >= initial_line and pos.line >= initial_line): ct_span[0] += 1 lt_span[0] += 1 initial_line += 1 ct_span[0] = max(0, ct_span[0] - 1) lt_span[0] = max(0, lt_span[0] - 1) initial_line = max(cstart, initial_line - 1) lt = lt[lt_span[0]:lt_span[1]] ct = ct[ct_span[0]:ct_span[1]] try: rv, es = guess_edit(initial_line, lt, ct, self._vstate) if not rv: lt = "\n".join(lt) ct = "\n".join(ct) es = diff(lt, ct, initial_line) self._active_snippets[0].replay_user_edits(es, self._ctab) except IndexError: # Rather do nothing than throwing an error. It will be correct # most of the time pass self._check_if_still_inside_snippet() if self._active_snippets: self._active_snippets[0].update_textobjects(vim_helper.buf) self._vstate.remember_buffer(self._active_snippets[0]) def _setup_inner_state(self): """Map keys and create autocommands that should only be defined when a snippet is active.""" if self._inner_state_up: return if self.expand_trigger != self.forward_trigger: vim_helper.command("inoremap <buffer><nowait><silent> " + self.forward_trigger + " <C-R>=UltiSnips#JumpForwards()<cr>") vim_helper.command("snoremap <buffer><nowait><silent> " + self.forward_trigger + " <Esc>:call UltiSnips#JumpForwards()<cr>") vim_helper.command("inoremap <buffer><nowait><silent> " + self.backward_trigger + " <C-R>=UltiSnips#JumpBackwards()<cr>") vim_helper.command("snoremap <buffer><nowait><silent> " + self.backward_trigger + " <Esc>:call UltiSnips#JumpBackwards()<cr>") # Setup the autogroups. vim_helper.command("augroup UltiSnips") vim_helper.command("autocmd!") vim_helper.command( "autocmd CursorMovedI * call UltiSnips#CursorMoved()") vim_helper.command( "autocmd CursorMoved * call UltiSnips#CursorMoved()") vim_helper.command( "autocmd InsertLeave * call UltiSnips#LeavingInsertMode()") vim_helper.command("autocmd BufEnter * call UltiSnips#LeavingBuffer()") vim_helper.command( "autocmd CmdwinEnter * call UltiSnips#LeavingBuffer()") vim_helper.command( "autocmd CmdwinLeave * call UltiSnips#LeavingBuffer()") # Also exit the snippet when we enter a unite complete buffer. vim_helper.command( "autocmd Filetype unite call UltiSnips#LeavingBuffer()") vim_helper.command("augroup END") vim_helper.command( "silent doautocmd <nomodeline> User UltiSnipsEnterFirstSnippet") self._inner_state_up = True def _teardown_inner_state(self): """Reverse _setup_inner_state.""" if not self._inner_state_up: return try: vim_helper.command( "silent doautocmd <nomodeline> User UltiSnipsExitLastSnippet") if self.expand_trigger != self.forward_trigger: vim_helper.command("iunmap <buffer> %s" % self.forward_trigger) vim_helper.command("sunmap <buffer> %s" % self.forward_trigger) vim_helper.command("iunmap <buffer> %s" % self.backward_trigger) vim_helper.command("sunmap <buffer> %s" % self.backward_trigger) vim_helper.command("augroup UltiSnips") vim_helper.command("autocmd!") vim_helper.command("augroup END") self._inner_state_up = False except vim_helper.error: # This happens when a preview window was opened. This issues # CursorMoved, but not BufLeave. We have no way to unmap, until we # are back in our buffer pass @err_to_scratch_buffer.wrap def _save_last_visual_selection(self): """This is called when the expand trigger is pressed in visual mode. Our job is to remember everything between '< and '> and pass it on to. ${VISUAL} in case it will be needed. """ self._visual_content.conserve() def _leaving_buffer(self): """Called when the user switches tabs/windows/buffers. It basically means that all snippets must be properly terminated. """ while self._active_snippets: self._current_snippet_is_done() self._reinit() def _reinit(self): """Resets transient state.""" self._ctab = None self._ignore_movements = False def _check_if_still_inside_snippet(self): """Checks if the cursor is outside of the current snippet.""" if self._current_snippet and ( not self._current_snippet.start <= vim_helper.buf.cursor <= self._current_snippet.end): self._current_snippet_is_done() self._reinit() self._check_if_still_inside_snippet() def _current_snippet_is_done(self): """The current snippet should be terminated.""" self._active_snippets.pop() if not self._active_snippets: self._teardown_inner_state() def _jump(self, backwards=False): """Helper method that does the actual jump.""" if self._should_update_textobjects: self._should_reset_visual = False self._cursor_moved() # we need to set 'onemore' there, because of limitations of the vim # API regarding cursor movements; without that test # 'CanExpandAnonSnippetInJumpActionWhileSelected' will fail with vim_helper.option_set_to("ve", "onemore"): jumped = False # We need to remember current snippets stack here because of # post-jump action on the last tabstop should be able to access # snippet instance which is ended just now. stack_for_post_jump = self._active_snippets[:] # If next tab has length 1 and the distance between itself and # self._ctab is 1 then there is 1 less CursorMove events. We # cannot ignore next movement in such case. ntab_short_and_near = False if self._current_snippet: snippet_for_action = self._current_snippet elif stack_for_post_jump: snippet_for_action = stack_for_post_jump[-1] else: snippet_for_action = None if self._current_snippet: ntab = self._current_snippet.select_next_tab(backwards) if ntab: if self._current_snippet.snippet.has_option("s"): lineno = vim_helper.buf.cursor.line vim_helper.buf[lineno] = vim_helper.buf[lineno].rstrip( ) vim_helper.select(ntab.start, ntab.end) jumped = True if (self._ctab is not None and ntab.start - self._ctab.end == Position(0, 1) and ntab.end - ntab.start == Position(0, 1)): ntab_short_and_near = True self._ctab = ntab # Run interpolations again to update new placeholder # values, binded to currently newly jumped placeholder. self._visual_content.conserve_placeholder(self._ctab) self._current_snippet.current_placeholder = ( self._visual_content.placeholder) self._should_reset_visual = False self._active_snippets[0].update_textobjects(vim_helper.buf) # Open any folds this might have created vim_helper.command("normal! zv") self._vstate.remember_buffer(self._active_snippets[0]) if ntab.number == 0 and self._active_snippets: self._current_snippet_is_done() else: # This really shouldn't happen, because a snippet should # have been popped when its final tabstop was used. # Cleanup by removing current snippet and recursing. self._current_snippet_is_done() jumped = self._jump(backwards) if jumped: if self._ctab: self._vstate.remember_position() self._vstate.remember_unnamed_register( self._ctab.current_text) if not ntab_short_and_near: self._ignore_movements = True if len(stack_for_post_jump) > 0 and ntab is not None: with use_proxy_buffer(stack_for_post_jump, self._vstate): snippet_for_action.snippet.do_post_jump( ntab.number, -1 if backwards else 1, stack_for_post_jump, snippet_for_action, ) return jumped def _leaving_insert_mode(self): """Called whenever we leave the insert mode.""" self._vstate.restore_unnamed_register() def _handle_failure(self, trigger): """Mainly make sure that we play well with SuperTab.""" if trigger.lower() == "<tab>": feedkey = "\\" + trigger elif trigger.lower() == "<s-tab>": feedkey = "\\" + trigger else: feedkey = None mode = "n" if not self._supertab_keys: if vim_helper.eval("exists('g:SuperTabMappingForward')") != "0": self._supertab_keys = ( vim_helper.eval("g:SuperTabMappingForward"), vim_helper.eval("g:SuperTabMappingBackward"), ) else: self._supertab_keys = ["", ""] for idx, sttrig in enumerate(self._supertab_keys): if trigger.lower() == sttrig.lower(): if idx == 0: feedkey = r"\<Plug>SuperTabForward" mode = "n" elif idx == 1: feedkey = r"\<Plug>SuperTabBackward" mode = "p" # Use remap mode so SuperTab mappings will be invoked. break if feedkey in (r"\<Plug>SuperTabForward", r"\<Plug>SuperTabBackward"): vim_helper.command("return SuperTab(%s)" % vim_helper.escape(mode)) elif feedkey: vim_helper.command("return %s" % vim_helper.escape(feedkey)) def _snips(self, before, partial, autotrigger_only=False): """Returns all the snippets for the given text before the cursor. If partial is True, then get also return partial matches. """ filetypes = self.get_buffer_filetypes()[::-1] matching_snippets = defaultdict(list) clear_priority = None cleared = {} for _, source in self._snippet_sources: source.ensure(filetypes) # Collect cleared information from sources. for _, source in self._snippet_sources: sclear_priority = source.get_clear_priority(filetypes) if sclear_priority is not None and ( clear_priority is None or sclear_priority > clear_priority): clear_priority = sclear_priority for key, value in source.get_cleared(filetypes).items(): if key not in cleared or value > cleared[key]: cleared[key] = value for _, source in self._snippet_sources: possible_snippets = source.get_snippets(filetypes, before, partial, autotrigger_only, self._visual_content) for snippet in possible_snippets: if (clear_priority is None or snippet.priority > clear_priority ) and (snippet.trigger not in cleared or snippet.priority > cleared[snippet.trigger]): matching_snippets[snippet.trigger].append(snippet) if not matching_snippets: return [] # Now filter duplicates and only keep the one with the highest # priority. snippets = [] for snippets_with_trigger in matching_snippets.values(): highest_priority = max(s.priority for s in snippets_with_trigger) snippets.extend(s for s in snippets_with_trigger if s.priority == highest_priority) # For partial matches we are done, but if we want to expand a snippet, # we have to go over them again and only keep those with the maximum # priority. if partial: return snippets highest_priority = max(s.priority for s in snippets) return [s for s in snippets if s.priority == highest_priority] def _do_snippet(self, snippet, before): """Expands the given snippet, and handles everything that needs to be done with it.""" self._setup_inner_state() self._snip_expanded_in_action = False self._should_update_textobjects = False # Adjust before, maybe the trigger is not the complete word text_before = before if snippet.matched: text_before = before[:-len(snippet.matched)] with use_proxy_buffer(self._active_snippets, self._vstate): with self._action_context(): cursor_set_in_action = snippet.do_pre_expand( self._visual_content.text, self._active_snippets) if cursor_set_in_action: text_before = vim_helper.buf.line_till_cursor before = vim_helper.buf.line_till_cursor with suspend_proxy_edits(): start = Position(vim_helper.buf.cursor.line, len(text_before)) end = Position(vim_helper.buf.cursor.line, len(before)) parent = None if self._current_snippet: # If cursor is set in pre-action, then action was modified # cursor line, in that case we do not need to do any edits, it # can break snippet if not cursor_set_in_action: # It could be that our trigger contains the content of # TextObjects in our containing snippet. If this is indeed # the case, we have to make sure that those are properly # killed. We do this by pretending that the user deleted # and retyped the text that our trigger matched. edit_actions = [ ("D", start.line, start.col, snippet.matched), ("I", start.line, start.col, snippet.matched), ] self._active_snippets[0].replay_user_edits(edit_actions) parent = self._current_snippet.find_parent_for_new_to(start) snippet_instance = snippet.launch(text_before, self._visual_content, parent, start, end) # Open any folds this might have created vim_helper.command("normal! zv") self._visual_content.reset() self._active_snippets.append(snippet_instance) with use_proxy_buffer(self._active_snippets, self._vstate): with self._action_context(): snippet.do_post_expand( snippet_instance.start, snippet_instance.end, self._active_snippets, ) self._vstate.remember_buffer(self._active_snippets[0]) if not self._snip_expanded_in_action: self._jump() elif self._current_snippet.current_text != "": self._jump() else: self._current_snippet_is_done() if self._inside_action: self._snip_expanded_in_action = True def _try_expand(self, autotrigger_only=False): """Try to expand a snippet in the current place.""" before = vim_helper.buf.line_till_cursor snippets = self._snips(before, False, autotrigger_only) if snippets: # prefer snippets with context if any snippets_with_context = [s for s in snippets if s.context] if snippets_with_context: snippets = snippets_with_context if not snippets: # No snippet found return False vim_helper.command("let &undolevels = &undolevels") if len(snippets) == 1: snippet = snippets[0] else: snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) vim_helper.command("let &undolevels = &undolevels") return True @property def _current_snippet(self): """The current snippet or None.""" if not self._active_snippets: return None return self._active_snippets[-1] def _file_to_edit(self, requested_ft, bang): """Returns a file to be edited for the given requested_ft. If 'bang' is empty a reasonable first choice is opened (see docs), otherwise all files are considered and the user gets to choose. """ filetypes = [] if requested_ft: filetypes.append(requested_ft) else: if bang: filetypes.extend(self.get_buffer_filetypes()) else: filetypes.append(self.get_buffer_filetypes()[0]) potentials = set() all_snippet_directories = find_all_snippet_directories() if len(all_snippet_directories) == 1: # Most likely the user has set g:UltiSnipsSnippetDirectories to a # single absolute path. potentials.update( _get_potential_snippet_filenames_to_edit( all_snippet_directories[0], filetypes)) else: # Likely the array contains things like ["UltiSnips", # "mycoolsnippets"] There is no more obvious way to edit than in # the users vim config directory. dot_vim_dir = Path(vim_helper.get_dot_vim()) for snippet_dir in all_snippet_directories: snippet_dir = Path(snippet_dir) if dot_vim_dir != snippet_dir.parent: continue potentials.update( _get_potential_snippet_filenames_to_edit( snippet_dir, filetypes)) if bang: for ft in filetypes: potentials.update(find_all_snippet_files(ft)) return _select_and_create_file_to_edit(potentials) @contextmanager def _action_context(self): try: old_flag = self._inside_action self._inside_action = True yield finally: self._inside_action = old_flag @err_to_scratch_buffer.wrap def _track_change(self): self._should_update_textobjects = True try: inserted_char = vim_helper.eval("v:char") except UnicodeDecodeError: return if isinstance(inserted_char, bytes): return try: if inserted_char == "": before = vim_helper.buf.line_till_cursor if (before and before[-1] == self._last_change[0] or self._last_change[1] != vim_helper.buf.cursor): self._try_expand(autotrigger_only=True) finally: self._last_change = (inserted_char, vim_helper.buf.cursor) if self._should_reset_visual and self._visual_content.mode == "": self._visual_content.reset() self._should_reset_visual = True @err_to_scratch_buffer.wrap def _refresh_snippets(self): for _, source in self._snippet_sources: source.refresh()
class SnippetManager(object): """The main entry point for all UltiSnips functionality. All Vim functions call methods in this class.""" def __init__(self, expand_trigger, forward_trigger, backward_trigger): self.expand_trigger = expand_trigger self.forward_trigger = forward_trigger self.backward_trigger = backward_trigger self._supertab_keys = None self._csnippets = [] self._buffer_filetypes = defaultdict(lambda: ['all']) self._vstate = VimState() self._visual_content = VisualContentPreserver() self._added_snippets_source = AddedSnippetsSource() self._snippet_sources = [ UltiSnipsFileSource(), self._added_snippets_source, SnipMateFileSource(), ] self._reinit() @err_to_scratch_buffer def jump_forwards(self): """Jumps to the next tabstop.""" _vim.command("let g:ulti_jump_forwards_res = 1") if not self._jump(): _vim.command("let g:ulti_jump_forwards_res = 0") return self._handle_failure(self.forward_trigger) @err_to_scratch_buffer def jump_backwards(self): """Jumps to the previous tabstop.""" _vim.command("let g:ulti_jump_backwards_res = 1") if not self._jump(True): _vim.command("let g:ulti_jump_backwards_res = 0") return self._handle_failure(self.backward_trigger) @err_to_scratch_buffer def expand(self): """Try to expand a snippet at the current position.""" _vim.command("let g:ulti_expand_res = 1") if not self._try_expand(): _vim.command("let g:ulti_expand_res = 0") self._handle_failure(self.expand_trigger) @err_to_scratch_buffer def expand_or_jump(self): """ This function is used for people who wants to have the same trigger for expansion and forward jumping. It first tries to expand a snippet, if this fails, it tries to jump forward. """ _vim.command('let g:ulti_expand_or_jump_res = 1') rv = self._try_expand() if not rv: _vim.command('let g:ulti_expand_or_jump_res = 2') rv = self._jump() if not rv: _vim.command('let g:ulti_expand_or_jump_res = 0') self._handle_failure(self.expand_trigger) @err_to_scratch_buffer def snippets_in_current_scope(self): """Returns the snippets that could be expanded to Vim as a global variable.""" before = _vim.buf.line_till_cursor snippets = self._snips(before, True) # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) for snip in snippets: description = snip.description[snip.description.find(snip.trigger) + len(snip.trigger) + 2:] key = as_unicode(snip.trigger) description = as_unicode(description) # remove surrounding "" or '' in snippet description if it exists if len(description) > 2: if (description[0] == description[-1] and description[0] in "'\""): description = description[1:-1] _vim.command(as_unicode( "let g:current_ulti_dict['{key}'] = '{val}'").format( key=key.replace("'", "''"), val=description.replace("'", "''"))) @err_to_scratch_buffer def list_snippets(self): """Shows the snippets that could be expanded to the User and let her select one.""" before = _vim.buf.line_till_cursor snippets = self._snips(before, True) if len(snippets) == 0: self._handle_failure(self.backward_trigger) return True # Sort snippets alphabetically snippets.sort(key=lambda x: x.trigger) if not snippets: return True snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) return True @err_to_scratch_buffer def add_snippet(self, trigger, value, description, options, ft="all", priority=0): """Add a snippet to the list of known snippets of the given 'ft'.""" self._added_snippets_source.add_snippet(ft, UltiSnipsSnippetDefinition(priority, trigger, value, description, options, {})) @err_to_scratch_buffer def expand_anon(self, value, trigger="", description="", options=""): """Expand an anonymous snippet right here.""" before = _vim.buf.line_till_cursor snip = UltiSnipsSnippetDefinition(0, trigger, value, description, options, {}) if not trigger or snip.matches(before): self._do_snippet(snip, before) return True else: return False def reset_buffer_filetypes(self): """Reset the filetypes for the current buffer.""" if _vim.buf.number in self._buffer_filetypes: del self._buffer_filetypes[_vim.buf.number] def add_buffer_filetypes(self, ft): """Checks for changes in the list of snippet files or the contents of the snippet files and reloads them if necessary. """ buf_fts = self._buffer_filetypes[_vim.buf.number] idx = -1 for ft in ft.split("."): ft = ft.strip() if not ft: continue try: idx = buf_fts.index(ft) except ValueError: self._buffer_filetypes[_vim.buf.number].insert(idx + 1, ft) idx += 1 @err_to_scratch_buffer def _cursor_moved(self): """Called whenever the cursor moved.""" self._vstate.remember_position() if _vim.eval("mode()") not in 'in': return if self._ignore_movements: self._ignore_movements = False return if self._csnippets: cstart = self._csnippets[0].start.line cend = self._csnippets[0].end.line + \ self._vstate.diff_in_buffer_length ct = _vim.buf[cstart:cend + 1] lt = self._vstate.remembered_buffer pos = _vim.buf.cursor lt_span = [0, len(lt)] ct_span = [0, len(ct)] initial_line = cstart # Cut down on lines searched for changes. Start from behind and # remove all equal lines. Then do the same from the front. if lt and ct: while (lt[lt_span[1]-1] == ct[ct_span[1]-1] and self._vstate.ppos.line < initial_line + lt_span[1]-1 and pos.line < initial_line + ct_span[1]-1 and (lt_span[0] < lt_span[1]) and (ct_span[0] < ct_span[1])): ct_span[1] -= 1 lt_span[1] -= 1 while (lt_span[0] < lt_span[1] and ct_span[0] < ct_span[1] and lt[lt_span[0]] == ct[ct_span[0]] and self._vstate.ppos.line >= initial_line and pos.line >= initial_line): ct_span[0] += 1 lt_span[0] += 1 initial_line += 1 ct_span[0] = max(0, ct_span[0] - 1) lt_span[0] = max(0, lt_span[0] - 1) initial_line = max(cstart, initial_line - 1) lt = lt[lt_span[0]:lt_span[1]] ct = ct[ct_span[0]:ct_span[1]] try: rv, es = guess_edit(initial_line, lt, ct, self._vstate) if not rv: lt = '\n'.join(lt) ct = '\n'.join(ct) es = diff(lt, ct, initial_line) self._csnippets[0].replay_user_edits(es) except IndexError: # Rather do nothing than throwing an error. It will be correct # most of the time pass self._check_if_still_inside_snippet() if self._csnippets: self._csnippets[0].update_textobjects() self._vstate.remember_buffer(self._csnippets[0]) @err_to_scratch_buffer def _save_last_visual_selection(self): """ This is called when the expand trigger is pressed in visual mode. Our job is to remember everything between '< and '> and pass it on to ${VISUAL} in case it will be needed. """ self._visual_content.conserve() def _leaving_buffer(self): """Called when the user switches tabs/windows/buffers. It basically means that all snippets must be properly terminated.""" while len(self._csnippets): self._current_snippet_is_done() self._reinit() def _reinit(self): """Resets transient state.""" self._ctab = None self._ignore_movements = False def _check_if_still_inside_snippet(self): """Checks if the cursor is outside of the current snippet.""" if self._cs and ( not self._cs.start <= _vim.buf.cursor <= self._cs.end ): self._current_snippet_is_done() self._reinit() self._check_if_still_inside_snippet() def _current_snippet_is_done(self): """The current snippet should be terminated.""" self._csnippets.pop() if not self._csnippets: _vim.command("call UltiSnips#map_keys#RestoreInnerKeys()") def _jump(self, backwards=False): """Helper method that does the actual jump.""" jumped = False if self._cs: self._ctab = self._cs.select_next_tab(backwards) if self._ctab: if self._cs.snippet.has_option("s"): lineno = _vim.buf.cursor.line _vim.buf[lineno] = _vim.buf[lineno].rstrip() _vim.select(self._ctab.start, self._ctab.end) jumped = True if self._ctab.number == 0: self._current_snippet_is_done() else: # This really shouldn't happen, because a snippet should # have been popped when its final tabstop was used. # Cleanup by removing current snippet and recursing. self._current_snippet_is_done() jumped = self._jump(backwards) if jumped: self._vstate.remember_position() self._vstate.remember_unnamed_register(self._ctab.current_text) self._ignore_movements = True return jumped def _leaving_insert_mode(self): """Called whenever we leave the insert mode.""" self._vstate.restore_unnamed_register() def _handle_failure(self, trigger): """Mainly make sure that we play well with SuperTab.""" if trigger.lower() == "<tab>": feedkey = "\\" + trigger elif trigger.lower() == "<s-tab>": feedkey = "\\" + trigger else: feedkey = None mode = "n" if not self._supertab_keys: if _vim.eval("exists('g:SuperTabMappingForward')") != "0": self._supertab_keys = ( _vim.eval("g:SuperTabMappingForward"), _vim.eval("g:SuperTabMappingBackward"), ) else: self._supertab_keys = ['', ''] for idx, sttrig in enumerate(self._supertab_keys): if trigger.lower() == sttrig.lower(): if idx == 0: feedkey = r"\<Plug>SuperTabForward" mode = "n" elif idx == 1: feedkey = r"\<Plug>SuperTabBackward" mode = "p" # Use remap mode so SuperTab mappings will be invoked. break if (feedkey == r"\<Plug>SuperTabForward" or feedkey == r"\<Plug>SuperTabBackward"): _vim.command("return SuperTab(%s)" % _vim.escape(mode)) elif feedkey: _vim.command("return %s" % _vim.escape(feedkey)) def _snips(self, before, possible): """ Returns all the snippets for the given text before the cursor. If possible is True, then get all possible matches. """ filetypes = self._buffer_filetypes[_vim.buf.number][::-1] matching_snippets = defaultdict(list) for source in self._snippet_sources: for snippet in source.get_snippets(filetypes, before, possible): matching_snippets[snippet.trigger].append(snippet) if not matching_snippets: return [] # Now filter duplicates and only keep the one with the highest # priority. Only keep the snippets with the highest priority. snippets = [] for snippets_with_trigger in matching_snippets.values(): highest_priority = max(s.priority for s in snippets_with_trigger) snippets.extend(s for s in snippets_with_trigger if s.priority == highest_priority) return snippets def _do_snippet(self, snippet, before): """Expands the given snippet, and handles everything that needs to be done with it.""" _vim.command("call UltiSnips#map_keys#MapInnerKeys()") # Adjust before, maybe the trigger is not the complete word text_before = before if snippet.matched: text_before = before[:-len(snippet.matched)] if self._cs: start = Position(_vim.buf.cursor.line, len(text_before)) end = Position(_vim.buf.cursor.line, len(before)) # It could be that our trigger contains the content of TextObjects # in our containing snippet. If this is indeed the case, we have to # make sure that those are properly killed. We do this by # pretending that the user deleted and retyped the text that our # trigger matched. edit_actions = [ ("D", start.line, start.col, snippet.matched), ("I", start.line, start.col, snippet.matched), ] self._csnippets[0].replay_user_edits(edit_actions) si = snippet.launch(text_before, self._visual_content, self._cs.find_parent_for_new_to(start), start, end) else: start = Position(_vim.buf.cursor.line, len(text_before)) end = Position(_vim.buf.cursor.line, len(before)) si = snippet.launch(text_before, self._visual_content, None, start, end) self._visual_content.reset() self._csnippets.append(si) self._ignore_movements = True self._vstate.remember_buffer(self._csnippets[0]) self._jump() def _try_expand(self): """Try to expand a snippet in the current place.""" before = _vim.buf.line_till_cursor if not before: return False snippets = self._snips(before, False) if not snippets: # No snippet found return False elif len(snippets) == 1: snippet = snippets[0] else: snippet = _ask_snippets(snippets) if not snippet: return True self._do_snippet(snippet, before) return True @property def _cs(self): """The current snippet or None.""" if not len(self._csnippets): return None return self._csnippets[-1] @property def _primary_filetype(self): """This filetype will be edited when UltiSnipsEdit is called without any arguments.""" return self._buffer_filetypes[_vim.buf.number][0] # TODO(sirver): this should talk directly to the UltiSnipsFileSource. def _file_to_edit(self, ft): # pylint: disable=no-self-use """ Gets a file to edit based on the given filetype. If no filetype is given, uses the current filetype from Vim. Checks 'g:UltiSnipsSnippetsDir' and uses it if it exists If a non-shipped file already exists, it uses it. Otherwise uses a file in ~/.vim/ or ~/vimfiles """ # This method is not using self, but is called by UltiSnips.vim and is # therefore in this class because it is the facade to Vim. edit = None existing = base_snippet_files_for(ft, False) filename = ft + ".snippets" if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == "1": snipdir = _vim.eval("g:UltiSnipsSnippetsDir") edit = os.path.join(snipdir, filename) elif existing: edit = existing[-1] # last sourced else: home = _vim.eval("$HOME") rtp = [os.path.realpath(os.path.expanduser(p)) for p in _vim.eval("&rtp").split(",")] snippet_dirs = ["UltiSnips"] + \ _vim.eval("g:UltiSnipsSnippetDirectories") us = snippet_dirs[-1] path = os.path.join(home, ".vim", us) for dirname in [".vim", "vimfiles"]: pth = os.path.join(home, dirname) if pth in rtp: path = os.path.join(pth, us) if not os.path.isdir(path): os.mkdir(path) edit = os.path.join(path, filename) return edit