From d2d4efda9de88fc1ca92cd0a10bdeea32f1ff18f Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Mon, 12 Apr 2021 13:07:11 -0400 Subject: [PATCH 01/10] Add SettingsListener. --- st3/sublime_lib/__init__.py | 3 + st3/sublime_lib/settings_listener.py | 85 +++++++++++++++++++ .../settings_listener_plugin.py | 20 +++++ tests/test_settings_listener.py | 33 +++++++ 4 files changed, 141 insertions(+) create mode 100644 st3/sublime_lib/settings_listener.py create mode 100644 tests/settings_listener_package/settings_listener_plugin.py create mode 100644 tests/test_settings_listener.py diff --git a/st3/sublime_lib/__init__.py b/st3/sublime_lib/__init__.py index b1f072f..03f3070 100644 --- a/st3/sublime_lib/__init__.py +++ b/st3/sublime_lib/__init__.py @@ -7,3 +7,6 @@ from .view_utils import new_view, close_view, LineEnding # noqa: F401 from .activity_indicator import ActivityIndicator # noqa: F401 from .window_utils import new_window, close_window # noqa: F401 +from .settings_listener import ( # noqa: F401 + GlobalSettingsListener, ViewSettingsListener, on_setting_changed +) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py new file mode 100644 index 0000000..2146e1b --- /dev/null +++ b/st3/sublime_lib/settings_listener.py @@ -0,0 +1,85 @@ +import sublime +import sublime_plugin + +from collections import namedtuple +from ._util.weak_method import weak_method + + +from ._compat.typing import Callable, Any, Iterator, Tuple, TypeVar, Union + + +__all__ = ['GlobalSettingsListener', 'ViewSettingsListener', 'on_setting_changed'] + + +OnChangedOptions = namedtuple('OnChangedOptions', ('selector')) +OPTIONS_ATTRIBUTE = '_sublime_lib_settings_listener_options' + + +class BaseSettingsListener: + def _handlers(self) -> Iterator[Tuple[str, Callable, OnChangedOptions]]: + for name in dir(self): + value = getattr(self, name) + options = getattr(value, OPTIONS_ATTRIBUTE, None) + + if not name.startswith('_') and options is not None: + yield name, value, options + + def __init__(self, settings: sublime.Settings, *args: Any, **kwargs: Any) -> None: + # Don't complain that object.__init__ doesn't take any args + super().__init__(*args, **kwargs) # type: ignore + self.settings = settings + + self.settings.add_on_change(str(id(self)), weak_method(self._on_settings_changed)) + + self._last_known_values = { + name: options.selector(self.settings) + for name, _, options in self._handlers() + } + + def __del__(self) -> None: + self.settings.clear_on_change(str(id(self))) + + def _on_settings_changed(self) -> None: + for name, handler, options in self._handlers(): + previous_value = self._last_known_values[name] + current_value = options.selector(self.settings) + + if current_value != previous_value: + self._last_known_values[name] = current_value + handler(current_value, previous_value) + + +class GlobalSettingsListener(BaseSettingsListener, sublime_plugin.EventListener): + SETTINGS_NAME = '' + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(sublime.load_settings(self.SETTINGS_NAME), *args, **kwargs) + + +class ViewSettingsListener(BaseSettingsListener, sublime_plugin.ViewEventListener): + def __init__(self, *args: Any, **kwargs: Any) -> None: + view = args[0] + assert isinstance(view, sublime.View) + super().__init__(view.settings(), *args, **kwargs) + + +Selected = TypeVar('Selected') +OnChangeListener = Callable[[BaseSettingsListener, Selected, Selected], None] + + +def on_setting_changed( + selector: Union[str, Callable[[sublime.Settings], Selected]] +) -> Callable[[OnChangeListener], OnChangeListener]: + if callable(selector): + selector_function = selector + elif isinstance(selector, str): + selector_str = selector + selector_function = lambda settings: settings.get(selector_str) + else: + raise TypeError('Selector must be a string or function.') + + def decorator(function: OnChangeListener) -> OnChangeListener: + setattr(function, OPTIONS_ATTRIBUTE, OnChangedOptions(selector_function)) + return function + + return decorator diff --git a/tests/settings_listener_package/settings_listener_plugin.py b/tests/settings_listener_package/settings_listener_plugin.py new file mode 100644 index 0000000..2af0bbb --- /dev/null +++ b/tests/settings_listener_package/settings_listener_plugin.py @@ -0,0 +1,20 @@ +from sublime_lib import ViewSettingsListener, GlobalSettingsListener, on_setting_changed + + +__all__ = ['FooSettingListener', 'GlobalFooSettingListener'] + + +class FooSettingListener(ViewSettingsListener): + @on_setting_changed('foo') + def foo_changed(self, new_value: object, old_value: object): + changes = self.settings.get('changes', []) + self.settings.set('changes', changes + [[new_value, old_value]]) + print('foo_changed', new_value, old_value, changes) + + +class GlobalFooSettingListener(GlobalSettingsListener): + SETTINGS_NAME = 'Baz.sublime-settings' + + @on_setting_changed('foobar') + def foo_changed(self, new_value: object, old_value: object): + print('foo_changed', new_value, old_value) diff --git a/tests/test_settings_listener.py b/tests/test_settings_listener.py new file mode 100644 index 0000000..bca9344 --- /dev/null +++ b/tests/test_settings_listener.py @@ -0,0 +1,33 @@ +import sublime +from sublime_lib import ResourcePath, new_view, close_view +from .temporary_package import TemporaryPackage + +from unittesting import DeferrableTestCase + + +class TestViewSettingsListener(DeferrableTestCase): + def setUp(self): + self.view = new_view(sublime.active_window()) + self.temporary_package = TemporaryPackage( + 'sublime_lib_settings_listener_test', + ResourcePath("Packages/sublime_lib/tests/settings_listener_package") + ) + self.temporary_package.create() + yield self.temporary_package.exists + + def tearDown(self): + self.temporary_package.destroy() + if getattr(self, 'view', None): + try: + close_view(self.view, force=True) + except ValueError: + pass + + def test_view_listener(self): + self.view.settings().set('foo', 'A') + self.view.settings().set('foo', 'B') + + self.assertEqual(self.view.settings().get('changes'), [ + ['A', None], + ['B', 'A'], + ]) From 9aa7da29e48e4fdd7e59bec5bf721aa023919612 Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Tue, 13 Apr 2021 13:49:21 -0400 Subject: [PATCH 02/10] Use get_selector. --- st3/sublime_lib/settings_listener.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index 2146e1b..1f5111a 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -2,7 +2,9 @@ import sublime_plugin from collections import namedtuple + from ._util.weak_method import weak_method +from ._util.collections import get_selector from ._compat.typing import Callable, Any, Iterator, Tuple, TypeVar, Union @@ -70,16 +72,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def on_setting_changed( selector: Union[str, Callable[[sublime.Settings], Selected]] ) -> Callable[[OnChangeListener], OnChangeListener]: - if callable(selector): - selector_function = selector - elif isinstance(selector, str): - selector_str = selector - selector_function = lambda settings: settings.get(selector_str) - else: - raise TypeError('Selector must be a string or function.') - def decorator(function: OnChangeListener) -> OnChangeListener: - setattr(function, OPTIONS_ATTRIBUTE, OnChangedOptions(selector_function)) + setattr(function, OPTIONS_ATTRIBUTE, OnChangedOptions(get_selector(selector))) return function return decorator From 11b67fde13e875d1ef85da0114db0cf2f8206152 Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Tue, 13 Apr 2021 17:06:59 -0400 Subject: [PATCH 03/10] More tests and fix for GlobalSettingsListener. --- st3/sublime_lib/settings_listener.py | 9 ++++ stubs/sublime_plugin.pyi | 3 ++ .../settings_listener_plugin.py | 18 ++++---- tests/test_settings_listener.py | 41 +++++++++++++++++++ 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index 1f5111a..e091f43 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -57,6 +57,15 @@ class GlobalSettingsListener(BaseSettingsListener, sublime_plugin.EventListener) def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(sublime.load_settings(self.SETTINGS_NAME), *args, **kwargs) + def _on_settings_changed(self) -> None: + if self in sublime_plugin.all_callbacks['on_new']: + super()._on_settings_changed() + else: + self.__del__() + + def on_new(self, view: sublime.View) -> None: + pass + class ViewSettingsListener(BaseSettingsListener, sublime_plugin.ViewEventListener): def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/stubs/sublime_plugin.pyi b/stubs/sublime_plugin.pyi index 6e6cf37..2140d1e 100644 --- a/stubs/sublime_plugin.pyi +++ b/stubs/sublime_plugin.pyi @@ -2,6 +2,9 @@ import sublime from typing import Optional, Union, List, Dict, Tuple +all_callbacks: Dict[str, List['EventListener']] + + class CommandInputHandler(): def name(self) -> str: ... def next_input(self, args: dict) -> Optional[CommandInputHandler]: ... diff --git a/tests/settings_listener_package/settings_listener_plugin.py b/tests/settings_listener_package/settings_listener_plugin.py index 2af0bbb..d9aab4f 100644 --- a/tests/settings_listener_package/settings_listener_plugin.py +++ b/tests/settings_listener_package/settings_listener_plugin.py @@ -1,20 +1,24 @@ -from sublime_lib import ViewSettingsListener, GlobalSettingsListener, on_setting_changed +import sublime_lib +from sublime_lib import on_setting_changed, ResourcePath __all__ = ['FooSettingListener', 'GlobalFooSettingListener'] -class FooSettingListener(ViewSettingsListener): +PACKAGE_NAME = ResourcePath.from_file_path(__file__).package + + +class FooSettingListener(sublime_lib.ViewSettingsListener): @on_setting_changed('foo') def foo_changed(self, new_value: object, old_value: object): changes = self.settings.get('changes', []) self.settings.set('changes', changes + [[new_value, old_value]]) - print('foo_changed', new_value, old_value, changes) -class GlobalFooSettingListener(GlobalSettingsListener): - SETTINGS_NAME = 'Baz.sublime-settings' +class GlobalFooSettingListener(sublime_lib.GlobalSettingsListener): + SETTINGS_NAME = PACKAGE_NAME + '.sublime-settings' - @on_setting_changed('foobar') + @on_setting_changed('foo') def foo_changed(self, new_value: object, old_value: object): - print('foo_changed', new_value, old_value) + changes = self.settings.get('changes', []) + self.settings.set('changes', changes + [[new_value, old_value]]) diff --git a/tests/test_settings_listener.py b/tests/test_settings_listener.py index bca9344..21617ad 100644 --- a/tests/test_settings_listener.py +++ b/tests/test_settings_listener.py @@ -27,7 +27,48 @@ def test_view_listener(self): self.view.settings().set('foo', 'A') self.view.settings().set('foo', 'B') + self.temporary_package.destroy() + yield lambda: not self.temporary_package.exists() + + self.view.settings().set('foo', 'C') + self.assertEqual(self.view.settings().get('changes'), [ ['A', None], ['B', 'A'], ]) + + +class TestGlobalSettingsListener(DeferrableTestCase): + def setUp(self): + name = 'TestGlobalSettingsListener:{}'.format(str(id(self))) + self.settings = sublime.load_settings(name + '.sublime-settings') + self.settings.set('changes', []) + self.settings.set('foo', None) + self.temporary_package = TemporaryPackage( + name, + ResourcePath("Packages/sublime_lib/tests/settings_listener_package") + ) + self.temporary_package.create() + yield self.temporary_package.exists + + def tearDown(self): + self.temporary_package.destroy() + + def test_global_listener(self): + # pass + self.settings.set('foo', 'A') + self.settings.set('foo', 'B') + + self.temporary_package.destroy() + yield lambda: not self.temporary_package.exists() + yield 100 + import gc + gc.collect() + yield 100 + + self.settings.set('foo', 'C') + + self.assertEqual(self.settings.get('changes'), [ + ['A', None], + ['B', 'A'], + ]) From 7ab35d05ff36ee90cd5d6375f7c5e7879aec0870 Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Mon, 19 Apr 2021 14:17:10 -0400 Subject: [PATCH 04/10] First draft of docs. --- st3/sublime_lib/settings_listener.py | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index e091f43..e031339 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -52,12 +52,33 @@ def _on_settings_changed(self) -> None: class GlobalSettingsListener(BaseSettingsListener, sublime_plugin.EventListener): + """ + A subclass of :class:`sublime_plugin.EventListener` + that also listens for changes in a global settings object. + + Subclasses must set the `SETTINGS_NAME` class variable to the name of the global settings. + + Example usage: + + .. code-block:: python + from sublime_lib import GlobalSettingsListener, on_setting_changed + + class JsCustomConfigurationsListener(GlobalSettingsListener) + SETTINGS_NAME = 'JS Custom.sublime-settings' + + @on_setting_changed('configurations') + def configurations_changed(self, new_configuration, old_configuration): + if self.settings.get('auto_build', False): + sublime.active_window().run_command('build_js_custom_syntaxes') + """ SETTINGS_NAME = '' def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(sublime.load_settings(self.SETTINGS_NAME), *args, **kwargs) def _on_settings_changed(self) -> None: + # Necessary because Sublime keeps EventListener objects alive unnecessarily. + # See https://github.com/sublimehq/sublime_text/issues/4078. if self in sublime_plugin.all_callbacks['on_new']: super()._on_settings_changed() else: @@ -68,6 +89,31 @@ def on_new(self, view: sublime.View) -> None: class ViewSettingsListener(BaseSettingsListener, sublime_plugin.ViewEventListener): + """ + A subclass of :class:`sublime_plugin.ViewEventListener` + that also listens for changes in the view settings. + + Example: + + .. code-block:: python + from sublime_lib import ViewSettingsListener, on_setting_changed + + class TabSizeListener(ViewSettingsListener): + @classmethod + def is_applicable(cls, settings): + return settings.get('translate_tabs_to_spaces', False) + + @classmethod + def applies_to_primary_view_only(cls): + return True + + @on_setting_changed('tab_size') + def tab_size_changed(self, new_value, old_value): + self.view.run_command('resize_existing_tabs', { + 'new_size': new_value, + 'old_size': old_value, + }) + """ def __init__(self, *args: Any, **kwargs: Any) -> None: view = args[0] assert isinstance(view, sublime.View) @@ -81,6 +127,21 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def on_setting_changed( selector: Union[str, Callable[[sublime.Settings], Selected]] ) -> Callable[[OnChangeListener], OnChangeListener]: + """ + A decorator function to mark a method + of a :class:`GlobalSettingsListener` or :class:`ViewSettingsListener` + as a setting change handler. + + The handler is not called every time the `settings` object changes, + but only when the value derived using the `selector` argument changes. + If `selector` is callable, then the derived value is ``selector(self)``. + If `selector` is a :class:`str`, + then the derived value is ``self.get(selector, None)``. + Otherwise, the derived value is ``projection(self, selector)``. + + The handler should accept two arguments: + the new derived value and the previous derived value. + """ def decorator(function: OnChangeListener) -> OnChangeListener: setattr(function, OPTIONS_ATTRIBUTE, OnChangedOptions(get_selector(selector))) return function From 1f7bc82ad8d4fc2bd99379bcd4fe13fd04969495 Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Mon, 19 Apr 2021 15:17:51 -0400 Subject: [PATCH 05/10] First draft of docs. --- docs/source/index.rst | 7 +++++++ docs/source/mocks/sublime_plugin.py | 19 +++++++++++++++++++ st3/sublime_lib/settings_listener.py | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 docs/source/mocks/sublime_plugin.py diff --git a/docs/source/index.rst b/docs/source/index.rst index 3fb376c..d477d01 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -58,6 +58,13 @@ Region manager .. autoclass:: sublime_lib.RegionManager +Settings listeners +------------------ + +.. autoclass:: sublime_lib.GlobalSettingsListener +.. autoclass:: sublime_lib.ViewSettingsListener +.. autofunction:: sublime_lib.on_setting_changed + :mod:`~sublime_lib.encodings` submodule --------------------------------------- diff --git a/docs/source/mocks/sublime_plugin.py b/docs/source/mocks/sublime_plugin.py new file mode 100644 index 0000000..6e2a731 --- /dev/null +++ b/docs/source/mocks/sublime_plugin.py @@ -0,0 +1,19 @@ +from sphinx.ext.autodoc.mock import _MockObject + +import sys + + +class MockType(_MockObject): + def __init__(self, name): + self.name = name + + def __repr__(self): + return self.name + + +class SublimePluginMock: + EventListener = MockType('sublime_plugin.EventListener') + ViewEventListener = MockType('sublime_plugin.ViewEventListener') + + +sys.modules[__name__] = SublimePluginMock() diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index e031339..bc15e88 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -61,6 +61,7 @@ class GlobalSettingsListener(BaseSettingsListener, sublime_plugin.EventListener) Example usage: .. code-block:: python + from sublime_lib import GlobalSettingsListener, on_setting_changed class JsCustomConfigurationsListener(GlobalSettingsListener) @@ -96,6 +97,7 @@ class ViewSettingsListener(BaseSettingsListener, sublime_plugin.ViewEventListene Example: .. code-block:: python + from sublime_lib import ViewSettingsListener, on_setting_changed class TabSizeListener(ViewSettingsListener): From 15803a85292334987cac2402efdcdededd071a2b Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Wed, 28 Apr 2021 19:46:57 -0400 Subject: [PATCH 06/10] Don't manually call __del__ --- st3/sublime_lib/settings_listener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index bc15e88..77d261e 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -83,7 +83,7 @@ def _on_settings_changed(self) -> None: if self in sublime_plugin.all_callbacks['on_new']: super()._on_settings_changed() else: - self.__del__() + self.settings.clear_on_change(str(id(self))) def on_new(self, view: sublime.View) -> None: pass From 50a2a019a66929be282d6d96d7027495128b59a9 Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Fri, 14 May 2021 13:29:27 -0400 Subject: [PATCH 07/10] Add error handling when SETTINGS_NAME is missing. --- st3/sublime_lib/settings_listener.py | 12 ++++++++++-- tests/test_settings_listener.py | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index 77d261e..f651a3a 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -7,7 +7,7 @@ from ._util.collections import get_selector -from ._compat.typing import Callable, Any, Iterator, Tuple, TypeVar, Union +from ._compat.typing import Callable, Any, Iterator, Tuple, TypeVar, Union, Optional __all__ = ['GlobalSettingsListener', 'ViewSettingsListener', 'on_setting_changed'] @@ -57,6 +57,7 @@ class GlobalSettingsListener(BaseSettingsListener, sublime_plugin.EventListener) that also listens for changes in a global settings object. Subclasses must set the `SETTINGS_NAME` class variable to the name of the global settings. + Otherwise, :exc:`RuntimeError` is raised. Example usage: @@ -72,9 +73,16 @@ def configurations_changed(self, new_configuration, old_configuration): if self.settings.get('auto_build', False): sublime.active_window().run_command('build_js_custom_syntaxes') """ - SETTINGS_NAME = '' + SETTINGS_NAME = None # type: Optional[str] def __init__(self, *args: Any, **kwargs: Any) -> None: + if type(self) is GlobalSettingsListener: + # If someone accidentally exports GlobalSettingsListener itself, + # don't attach a listener. + return + elif self.SETTINGS_NAME is None: + raise RuntimeError('Must specify SETTINGS_NAME for listener {}.'.format(type(self).__name__)) + super().__init__(sublime.load_settings(self.SETTINGS_NAME), *args, **kwargs) def _on_settings_changed(self) -> None: diff --git a/tests/test_settings_listener.py b/tests/test_settings_listener.py index 21617ad..9444198 100644 --- a/tests/test_settings_listener.py +++ b/tests/test_settings_listener.py @@ -2,8 +2,11 @@ from sublime_lib import ResourcePath, new_view, close_view from .temporary_package import TemporaryPackage +from unittest import TestCase from unittesting import DeferrableTestCase +from sublime_lib import GlobalSettingsListener + class TestViewSettingsListener(DeferrableTestCase): def setUp(self): @@ -55,7 +58,6 @@ def tearDown(self): self.temporary_package.destroy() def test_global_listener(self): - # pass self.settings.set('foo', 'A') self.settings.set('foo', 'B') @@ -72,3 +74,15 @@ def test_global_listener(self): ['A', None], ['B', 'A'], ]) + + +class TestGlobalSettingsListenerErrors(TestCase): + def test_instantiate_base(self): + GlobalSettingsListener() + + def test_instantiate_without_name(self): + class TestListener(GlobalSettingsListener): + pass + + with self.assertRaises(RuntimeError): + TestListener() From 2571d551852ccd6e85815aa06157469b3e81eb4e Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Fri, 14 May 2021 13:33:41 -0400 Subject: [PATCH 08/10] Fix line length. --- st3/sublime_lib/settings_listener.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index f651a3a..2818e6f 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -81,7 +81,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # don't attach a listener. return elif self.SETTINGS_NAME is None: - raise RuntimeError('Must specify SETTINGS_NAME for listener {}.'.format(type(self).__name__)) + listener_name = type(self).__name__ + raise RuntimeError('Must specify SETTINGS_NAME for listener {}.'.format(listener_name)) super().__init__(sublime.load_settings(self.SETTINGS_NAME), *args, **kwargs) From 031f49b27ab487c9aef421a2394c27842cdc3b00 Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Fri, 21 May 2021 14:11:47 -0400 Subject: [PATCH 09/10] Add projection documentation. --- st3/sublime_lib/settings_listener.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index 2818e6f..c334821 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -145,13 +145,29 @@ def on_setting_changed( The handler is not called every time the `settings` object changes, but only when the value derived using the `selector` argument changes. - If `selector` is callable, then the derived value is ``selector(self)``. + If `selector` is callable, then the derived value is ``selector(settings)``. If `selector` is a :class:`str`, - then the derived value is ``self.get(selector, None)``. - Otherwise, the derived value is ``projection(self, selector)``. + then the derived value is ``settings.get(selector, None)``. + Otherwise, the derived value is ``projection(settings, selector)`` (as defined below). The handler should accept two arguments: the new derived value and the previous derived value. + + ``projection(settings, keys)`` + Return a new :class:`dict` with keys of ``settings`` restricted to values in ``keys``. + + .. code-block:: python + + >>> projection({'a': 1, 'b': 2}, ['b']) + {'b': 2} + + If ``keys`` is a :class:`dict`, then it maps keys of the original dict to + keys of the result: + + .. code-block:: python + + >>> projection({'a': 1, 'b': 2}, {'b': 'c'}) + {'c': 2} """ def decorator(function: OnChangeListener) -> OnChangeListener: setattr(function, OPTIONS_ATTRIBUTE, OnChangedOptions(get_selector(selector))) From 508d6858a5913acedf6f3deece5d842af87a61cc Mon Sep 17 00:00:00 2001 From: Thom Smith Date: Sun, 4 Jul 2021 22:06:45 -0400 Subject: [PATCH 10/10] Move string case to top of projection docs. --- st3/sublime_lib/settings_listener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st3/sublime_lib/settings_listener.py b/st3/sublime_lib/settings_listener.py index c334821..29e6119 100644 --- a/st3/sublime_lib/settings_listener.py +++ b/st3/sublime_lib/settings_listener.py @@ -145,9 +145,9 @@ def on_setting_changed( The handler is not called every time the `settings` object changes, but only when the value derived using the `selector` argument changes. - If `selector` is callable, then the derived value is ``selector(settings)``. If `selector` is a :class:`str`, then the derived value is ``settings.get(selector, None)``. + If `selector` is callable, then the derived value is ``selector(settings)``. Otherwise, the derived value is ``projection(settings, selector)`` (as defined below). The handler should accept two arguments: