autotrigger: make snippets expand without <tab>

This commit is contained in:
Stanislav Seletskiy 2015-06-17 22:56:46 +06:00
parent e1e005a810
commit 76ebfec3cf
8 changed files with 168 additions and 10 deletions

View File

@ -151,3 +151,8 @@ endf
function! UltiSnips#LeavingInsertMode() function! UltiSnips#LeavingInsertMode()
exec g:_uspy "UltiSnips_Manager._leaving_insert_mode()" exec g:_uspy "UltiSnips_Manager._leaving_insert_mode()"
endfunction endfunction
function! UltiSnips#TrackChange()
exec g:_uspy "UltiSnips_Manager._track_change()"
endfunction
" }}}

View File

@ -42,6 +42,7 @@ UltiSnips *snippet* *snippets* *UltiSnips*
4.10.1 Pre-expand actions |UltiSnips-pre-expand-actions| 4.10.1 Pre-expand actions |UltiSnips-pre-expand-actions|
4.10.2 Post-expand actions |UltiSnips-post-expand-actions| 4.10.2 Post-expand actions |UltiSnips-post-expand-actions|
4.10.3 Post-jump actions |UltiSnips-post-jump-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. UltiSnips and Other Plugins |UltiSnips-other-plugins|
5.1 Existing Integrations |UltiSnips-integrations| 5.1 Existing Integrations |UltiSnips-integrations|
5.2 Extending UltiSnips |UltiSnips-extending| 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 python expression. This option can be specified along with other
options, like 'b'. See |UltiSnips-context-snippets| for more info. 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. > The end line is the 'endsnippet' keyword on a line by itself. >
endsnippet endsnippet
@ -1599,6 +1603,41 @@ def $1():
endsnippet endsnippet
------------------- SNAP ------------------- ------------------- 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* 5. UltiSnips and Other Plugins *UltiSnips-other-plugins*

View File

@ -46,6 +46,12 @@ command! -bang -nargs=? -complete=customlist,UltiSnips#FileTypeComplete UltiSnip
command! -nargs=1 UltiSnipsAddFiletypes :call UltiSnips#AddFiletypes(<q-args>) command! -nargs=1 UltiSnipsAddFiletypes :call UltiSnips#AddFiletypes(<q-args>)
augroup UltiSnips_AutoTrigger
au!
au InsertCharPre * call UltiSnips#TrackChange()
au TextChangedI * call UltiSnips#TrackChange()
augroup END
call UltiSnips#map_keys#MapKeys() call UltiSnips#map_keys#MapKeys()
" vim: ts=8 sts=4 sw=4 " vim: ts=8 sts=4 sw=4

View File

@ -23,12 +23,15 @@ class SnippetSource(object):
""" """
def loaded(self, filetypes):
return len(self._snippets) > 0
def _get_existing_deep_extends(self, base_filetypes): def _get_existing_deep_extends(self, base_filetypes):
"""Helper for get all existing filetypes extended by base filetypes.""" """Helper for get all existing filetypes extended by base filetypes."""
deep_extends = self.get_deep_extends(base_filetypes) deep_extends = self.get_deep_extends(base_filetypes)
return [ft for ft in deep_extends if ft in self._snippets] 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 """Returns the snippets for all 'filetypes' (in order) and their
parents matching the text 'before'. If 'possible' is true, a partial parents matching the text 'before'. If 'possible' is true, a partial
match is enough. Base classes can override this method to provide means match is enough. Base classes can override this method to provide means
@ -40,7 +43,8 @@ class SnippetSource(object):
result = [] result = []
for ft in self._get_existing_deep_extends(filetypes): for ft in self._get_existing_deep_extends(filetypes):
snips = self._snippets[ft] snips = self._snippets[ft]
result.extend(snips.get_matching_snippets(before, possible)) result.extend(snips.get_matching_snippets(before, possible,
autotrigger_only))
return result return result
def get_clear_priority(self, filetypes): def get_clear_priority(self, filetypes):

View File

@ -16,13 +16,23 @@ class SnippetDictionary(object):
"""Add 'snippet' to this dictionary.""" """Add 'snippet' to this dictionary."""
self._snippets.append(snippet) 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. """Returns all snippets matching the given trigger.
If 'potentially' is true, returns all that could_match(). 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 all_snippets = self._snippets
if autotrigger_only:
all_snippets = [s for s in all_snippets if s.has_option('A')]
if not potentially: if not potentially:
return [s for s in all_snippets if s.matches(trigger)] return [s for s in all_snippets if s.matches(trigger)]
else: else:
@ -43,3 +53,6 @@ class SnippetDictionary(object):
if (trigger not in self._cleared or if (trigger not in self._cleared or
priority > self._cleared[trigger]): priority > self._cleared[trigger]):
self._cleared[trigger] = priority self._cleared[trigger] = priority
def __len__(self):
return len(self._snippets)

View File

@ -100,6 +100,8 @@ class SnippetManager(object):
self._snip_expanded_in_action = False self._snip_expanded_in_action = False
self._inside_action = False self._inside_action = False
self._last_inserted_char = ''
self._added_snippets_source = AddedSnippetsSource() self._added_snippets_source = AddedSnippetsSource()
self.register_snippet_source('ultisnips_files', UltiSnipsFileSource()) self.register_snippet_source('ultisnips_files', UltiSnipsFileSource())
self.register_snippet_source('added', self._added_snippets_source) self.register_snippet_source('added', self._added_snippets_source)
@ -541,7 +543,7 @@ class SnippetManager(object):
elif feedkey: elif feedkey:
_vim.command('return %s' % _vim.escape(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. """Returns all the snippets for the given text before the cursor.
If partial is True, then get also return partial matches. If partial is True, then get also return partial matches.
@ -552,7 +554,8 @@ class SnippetManager(object):
clear_priority = None clear_priority = None
cleared = {} cleared = {}
for _, source in self._snippet_sources: 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. # Collect cleared information from sources.
for _, source in self._snippet_sources: for _, source in self._snippet_sources:
@ -565,7 +568,14 @@ class SnippetManager(object):
cleared[key] = value cleared[key] = value
for _, source in self._snippet_sources: 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) if ((clear_priority is None or snippet.priority > clear_priority)
and (snippet.trigger not in cleared or and (snippet.trigger not in cleared or
snippet.priority > cleared[snippet.trigger])): snippet.priority > cleared[snippet.trigger])):
@ -667,10 +677,10 @@ class SnippetManager(object):
self._snip_expanded_in_action = True 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.""" """Try to expand a snippet in the current place."""
before = _vim.buf.line_till_cursor before = _vim.buf.line_till_cursor
snippets = self._snips(before, False) snippets = self._snips(before, False, autotrigger_only)
if snippets: if snippets:
# prefer snippets with context if any # prefer snippets with context if any
snippets_with_context = [s for s in snippets if s.context] snippets_with_context = [s for s in snippets if s.context]
@ -765,6 +775,18 @@ class SnippetManager(object):
finally: finally:
self._inside_action = old_flag 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 UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name
vim.eval('g:UltiSnipsExpandTrigger'), vim.eval('g:UltiSnipsExpandTrigger'),
vim.eval('g:UltiSnipsJumpForwardTrigger'), vim.eval('g:UltiSnipsJumpForwardTrigger'),

69
test/test_Autotrigger.py Normal file
View File

@ -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'

View File

@ -154,8 +154,8 @@ from UltiSnips.snippet.source import SnippetSource
from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition
class MySnippetSource(SnippetSource): class MySnippetSource(SnippetSource):
def get_snippets(self, filetypes, before, possible): def get_snippets(self, filetypes, before, possible, autotrigger_only):
if before.endswith('blumba'): if before.endswith('blumba') and autotrigger_only == False:
return [ return [
UltiSnipsSnippetDefinition( UltiSnipsSnippetDefinition(
-100, "blumba", "this is a dynamic snippet", "", "", {}, "blub", -100, "blumba", "this is a dynamic snippet", "", "", {}, "blub",