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()
exec g:_uspy "UltiSnips_Manager._leaving_insert_mode()"
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.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*

View File

@ -46,6 +46,12 @@ command! -bang -nargs=? -complete=customlist,UltiSnips#FileTypeComplete UltiSnip
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()
" 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):
"""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):

View File

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

View File

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

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
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",