Since this is a recurring problem with writing plugins that listen to changes to the buffer (selection) but do not want to execute on every change, I’ve written a small self-sufficient decorator that you can use on such events to debounce its execution. It may or may not make it into the sublime_lib
dependency library available to Package Control at some point.
See the post linked below for the latest version.
from functools import partial, wraps
import sublime
import sublime_plugin
def _debounced_callback(view, old_change_count, callback):
if view.is_valid() and view.change_count() == old_change_count:
callback()
def debounced(delay_in_ms, sync=False):
"""Delay calls to on_(selection_)modified(_async) event hooks until they weren't triggered for n ms.
Works on both `EventListener` and `ViewEventListener` classes.
Calls are only made when the `view` is still "valid" according to ST's API,
so it's not necessary to check it in the wrapped function.
"""
def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
view = self.view if hasattr(self, 'view') else args[0]
if not view.is_valid():
return
change_count = view.change_count()
callback = partial(func, self, *args, **kwargs)
set_timeout = sublime.set_timeout if sync else sublime.set_timeout_async
set_timeout(partial(_debounced_callback, view, change_count, callback), delay_in_ms)
return wrapper
return decorator
class DebouncedListener(sublime_plugin.EventListener):
@debounced(500)
def on_modified_async(self, view):
print("debounced on EventListener", view.id())
class DebouncedViewListener(sublime_plugin.ViewEventListener):
@debounced(1000)
def on_modified_async(self):
print("debounced on ViewEventListener", self.view.id())