diff --git a/autoload/UltiSnips.vim b/autoload/UltiSnips.vim index b030d04..8fdf9c1 100644 --- a/autoload/UltiSnips.vim +++ b/autoload/UltiSnips.vim @@ -151,3 +151,8 @@ endf function! UltiSnips#LeavingInsertMode() exec g:_uspy "UltiSnips_Manager._leaving_insert_mode()" endfunction + +function! UltiSnips#TrackChange() + exec g:_uspy "UltiSnips_Manager._track_change()" +endfunction +" }}} diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index 1bf2b72..08adc68 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -42,6 +42,7 @@ UltiSnips *snippet* *snippets* *UltiSnips* 4.10.1 Pre-expand actions |UltiSnips-pre-expand-actions| 4.10.2 Post-expand actions |UltiSnips-post-expand-actions| 4.10.3 Post-jump actions |UltiSnips-post-jump-actions| + 4.11 Autotrigger |UltiSnips-autotrigger| 5. UltiSnips and Other Plugins |UltiSnips-other-plugins| 5.1 Existing Integrations |UltiSnips-integrations| 5.2 Extending UltiSnips |UltiSnips-extending| @@ -690,6 +691,9 @@ The options currently supported are: > python expression. This option can be specified along with other options, like 'b'. See |UltiSnips-context-snippets| for more info. + A Snippet will be triggered automatically, when condition matches. + See |UltiSnips-autotrigger| for more info. + The end line is the 'endsnippet' keyword on a line by itself. > endsnippet @@ -1599,6 +1603,41 @@ def $1(): endsnippet ------------------- SNAP ------------------- +4.11 Autotrigger *UltiSnips-autotrigger* +---------------- + +Note: vim should be newer than 7.4.214 to support this feature. + +Many language constructs can occur only at specific places, so it's +possible to use snippets without manually triggering them. + +Snippet can be marked as autotriggered by specifying 'A' option in the snippet +definition. + +After snippet is defined as being autotriggered, snippet condition will be +checked on every typed character and if condition matches, then snippet will +be triggered. + +*Warning:* using of this feature can lead to significant vim slowdown. If you +discovered that, report an issue to the github.com/SirVer/UltiSnips. + +Consider following snippets, that can be usefull in Go programming: +------------------- SNIP ------------------- +snippet "^p" "package" rbA +package ${1:main} +endsnippet + +snippet "^m" "func main" rbA +func main() { + $1 +} +endsnippet +------------------- SNAP ------------------- + +When "p" character will occur in the beginning of the line, it will be +automatically expanded into "package main". Same with "m" character. There is +no need to press trigger key after "m"" + ============================================================================== 5. UltiSnips and Other Plugins *UltiSnips-other-plugins* diff --git a/plugin/UltiSnips.vim b/plugin/UltiSnips.vim index a32b018..f7be308 100644 --- a/plugin/UltiSnips.vim +++ b/plugin/UltiSnips.vim @@ -46,6 +46,12 @@ command! -bang -nargs=? -complete=customlist,UltiSnips#FileTypeComplete UltiSnip command! -nargs=1 UltiSnipsAddFiletypes :call UltiSnips#AddFiletypes() +augroup UltiSnips_AutoTrigger + au! + au InsertCharPre * call UltiSnips#TrackChange() + au TextChangedI * call UltiSnips#TrackChange() +augroup END + call UltiSnips#map_keys#MapKeys() " vim: ts=8 sts=4 sw=4 diff --git a/pythonx/UltiSnips/snippet/source/_base.py b/pythonx/UltiSnips/snippet/source/_base.py index 71ecf5c..c4141eb 100644 --- a/pythonx/UltiSnips/snippet/source/_base.py +++ b/pythonx/UltiSnips/snippet/source/_base.py @@ -23,12 +23,15 @@ class SnippetSource(object): """ + def loaded(self, filetypes): + return len(self._snippets) > 0 + def _get_existing_deep_extends(self, base_filetypes): """Helper for get all existing filetypes extended by base filetypes.""" deep_extends = self.get_deep_extends(base_filetypes) return [ft for ft in deep_extends if ft in self._snippets] - def get_snippets(self, filetypes, before, possible): + def get_snippets(self, filetypes, before, possible, autotrigger_only): """Returns the snippets for all 'filetypes' (in order) and their parents matching the text 'before'. If 'possible' is true, a partial match is enough. Base classes can override this method to provide means @@ -40,7 +43,8 @@ class SnippetSource(object): result = [] for ft in self._get_existing_deep_extends(filetypes): snips = self._snippets[ft] - result.extend(snips.get_matching_snippets(before, possible)) + result.extend(snips.get_matching_snippets(before, possible, + autotrigger_only)) return result def get_clear_priority(self, filetypes): diff --git a/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py b/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py index a8b3d65..b5b9a5b 100644 --- a/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py +++ b/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py @@ -16,13 +16,23 @@ class SnippetDictionary(object): """Add 'snippet' to this dictionary.""" self._snippets.append(snippet) - def get_matching_snippets(self, trigger, potentially): + def get_matching_snippets(self, trigger, potentially, autotrigger_only): """Returns all snippets matching the given trigger. If 'potentially' is true, returns all that could_match(). + If 'autotrigger_only' is true, function will return only snippets which + are marked with flag 'A' (should be automatically expanded without + trigger key press). + It's handled specially to avoid walking down the list of all snippets, + which can be very slow, because function will be called on each change + made in insert mode. + """ all_snippets = self._snippets + if autotrigger_only: + all_snippets = [s for s in all_snippets if s.has_option('A')] + if not potentially: return [s for s in all_snippets if s.matches(trigger)] else: @@ -43,3 +53,6 @@ class SnippetDictionary(object): if (trigger not in self._cleared or priority > self._cleared[trigger]): self._cleared[trigger] = priority + + def __len__(self): + return len(self._snippets) diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index f84c48f..e54fd80 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -100,6 +100,8 @@ class SnippetManager(object): 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) @@ -541,7 +543,7 @@ class SnippetManager(object): elif feedkey: _vim.command('return %s' % _vim.escape(feedkey)) - def _snips(self, before, partial): + 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. @@ -552,7 +554,8 @@ class SnippetManager(object): clear_priority = None cleared = {} for _, source in self._snippet_sources: - source.ensure(filetypes) + if not autotrigger_only or not source.loaded(filetypes): + source.ensure(filetypes) # Collect cleared information from sources. for _, source in self._snippet_sources: @@ -565,7 +568,14 @@ class SnippetManager(object): cleared[key] = value for _, source in self._snippet_sources: - for snippet in source.get_snippets(filetypes, before, partial): + possible_snippets = source.get_snippets( + filetypes, + before, + partial, + autotrigger_only + ) + + 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])): @@ -667,10 +677,10 @@ class SnippetManager(object): self._snip_expanded_in_action = True - def _try_expand(self): + 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) + 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] @@ -765,6 +775,18 @@ class SnippetManager(object): finally: self._inside_action = old_flag + @err_to_scratch_buffer + def _track_change(self): + 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 + + UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name vim.eval('g:UltiSnipsExpandTrigger'), vim.eval('g:UltiSnipsJumpForwardTrigger'), diff --git a/test/test_Autotrigger.py b/test/test_Autotrigger.py new file mode 100644 index 0000000..86a1e2b --- /dev/null +++ b/test/test_Autotrigger.py @@ -0,0 +1,69 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +import subprocess + + +def has_patch(version, executable): + output = subprocess.check_output([executable, "--version"]) + patch = 1 + for line in output.decode('utf-8').split("\n"): + if line.startswith("Included patches:"): + patch = line.split('-')[1] + + return int(patch) >= version + + +def check_required_vim_version(test): + if test.vim_flavor == 'neovim': + return None + if not has_patch(214, test.vim._vim_executable): + return 'Vim newer than 7.4.214 is required' + else: + return None + + +class Autotrigger_CanMatchSimpleTrigger(_VimTest): + skip_if = check_required_vim_version + files = { 'us/all.snippets': r""" + snippet a "desc" A + autotriggered + endsnippet + """} + keys = 'a' + wanted = 'autotriggered' + + +class Autotrigger_CanMatchContext(_VimTest): + skip_if = check_required_vim_version + files = { 'us/all.snippets': r""" + snippet a "desc" "snip.line == 2" Ae + autotriggered + endsnippet + """} + keys = 'a\na' + wanted = 'autotriggered\na' + + +class Autotrigger_CanExpandOnTriggerWithLengthMoreThanOne(_VimTest): + skip_if = check_required_vim_version + files = { 'us/all.snippets': r""" + snippet abc "desc" A + autotriggered + endsnippet + """} + keys = 'abc' + wanted = 'autotriggered' + + +class Autotrigger_WillProduceNoExceptionWithVimLowerThan214(_VimTest): + skip_if = lambda self: 'Vim older than 7.4.214 is required' \ + if has_patch(214, self.vim._vim_executable) else None + + files = { 'us/all.snippets': r""" + snippet abc "desc" A + autotriggered + endsnippet + """} + keys = 'abc' + wanted = 'abc' diff --git a/test/test_UltiSnipFunc.py b/test/test_UltiSnipFunc.py index 6e6a06e..a0dde3d 100644 --- a/test/test_UltiSnipFunc.py +++ b/test/test_UltiSnipFunc.py @@ -154,8 +154,8 @@ from UltiSnips.snippet.source import SnippetSource from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition class MySnippetSource(SnippetSource): - def get_snippets(self, filetypes, before, possible): - if before.endswith('blumba'): + def get_snippets(self, filetypes, before, possible, autotrigger_only): + if before.endswith('blumba') and autotrigger_only == False: return [ UltiSnipsSnippetDefinition( -100, "blumba", "this is a dynamic snippet", "", "", {}, "blub",